From ba4e5e82700e39e7d50c61ea79bb3579704818a2 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 4 May 2026 15:34:35 +0300 Subject: [PATCH 01/67] Set up Docusaurus versioning: v2 default, v3 (Beta) at /v3 Snapshot current docs as version 2.x (served at site root) and configure the live docs/ folder as the in-progress v3 (Beta), served at /v3 with the unreleased banner. Adds a version dropdown to the navbar. --- docusaurus.config.js | 17 + .../01-getting-started/01-introduction.md | 69 + .../02-quick-start-guide.md | 289 +++++ .../04-using-the-enjin-platform.md | 589 +++++++++ .../01-how-to-use-graphql.md | 792 ++++++++++++ .../05-using-enjin-api/05-using-enjin-api.md | 98 ++ .../05-using-enjin-api/_category_.json | 3 + .../06-using-wallet-daemon.md | 172 +++ .../01-getting-started/_category_.json | 8 + .../01-creating-collections.md | 271 ++++ .../010-transfer-accept-collection.md | 343 +++++ .../02-creating-a-currency-token.md | 286 +++++ .../02-creating-tokens/02-creating-tokens.md | 364 ++++++ .../02-creating-tokens/_category_.json | 4 + .../01-managing-tokens/03-adding-metadata.md | 456 +++++++ .../01-managing-tokens/04-minting-a-token.md | 339 +++++ .../05-transferring-tokens.md | 724 +++++++++++ .../01-managing-tokens/06-create-qr-drops.md | 995 +++++++++++++++ .../01-managing-tokens/07-freezing-thawing.md | 1042 +++++++++++++++ .../08-burning-destroying-tokens.md | 640 ++++++++++ .../09-fetching-token-holders.md | 670 ++++++++++ .../01-managing-tokens/_category_.json | 4 + .../01-using-wallet-connect.md | 237 ++++ .../02-verifying-wallets.md | 717 +++++++++++ .../_category_.json | 3 + .../02-reading-user-wallets.md | 915 ++++++++++++++ .../03-using-managed-wallets.md | 975 ++++++++++++++ .../02-managing-users/04-using-fuel-tanks.md | 1057 ++++++++++++++++ .../02-managing-users/_category_.json | 3 + .../01-tokenid-structure.md | 188 +++ .../02-metadata-example.md | 104 ++ .../02-metadata-standard.md | 79 ++ .../03-dynamic-metadata.md | 188 +++ .../02-metadata-standard/_category_.json | 3 + .../03-enforced-rarity.md | 31 + .../04-brand-collab-nfts.md | 218 ++++ .../03-advanced-mechanics/05-enj-infusion.md | 224 ++++ .../06-the-multiverse.md | 169 +++ .../03-advanced-mechanics/_category_.json | 3 + ..._dispatch-transactions-using-fuel-tanks.md | 5 + .../01-getting-started.md | 208 +++ .../02-graphql-requests.md | 467 +++++++ .../03-platform-events.md | 392 ++++++ .../04-software-development-kit.md | 15 + .../_category_.json | 3 + .../04-software-development-kit/_logger.md | 170 +++ .../01-setup-guide.md | 179 +++ .../03-implementation-breakdown.md | 185 +++ .../_category_.json | 3 + .../01-platform/06-enjin-discord-bot.md | 62 + .../02-guides/01-platform/_category_.json | 8 + .../02-staking-integrtion-guide.md | 669 ++++++++++ .../03-creating-nomination-pools.md | 91 ++ .../04-joining-nomination-pools.md | 85 ++ .../01-relaychain/_category_.json | 7 + .../01-enjin-blockchain-nodes.md | 54 + .../01-running-a-node.md | 67 + .../02-run-relaychain-node.md | 128 ++ .../03-run-matrixchain-node.md | 128 ++ .../01-enjin-blockchain-nodes/_category_.json | 4 + .../01-running-a-validator.md | 210 ++++ ...-security-best-practices-for-validators.md | 48 + .../03-how-to-stop-validating.md | 66 + .../_category_.json | 7 + .../02-running-nodes/_category_.json | 7 + .../02-blockchain/03-running-enjin-indexer.md | 220 ++++ .../02-guides/02-blockchain/_category_.json | 8 + .../version-2.x/02-guides/_category_.json | 7 + .../01-queries/01-transactions-queries.md | 1116 +++++++++++++++++ .../01-queries/02-collections-queries.md | 405 ++++++ .../01-queries/03-tokens-queries.md | 317 +++++ .../01-queries/04-wallets-queries.md | 990 +++++++++++++++ .../01-queries/05-fuel-tank-queries.md | 283 +++++ .../01-queries/06-marketplace-queries.md | 593 +++++++++ .../01-queries/07-beam-queries.md | 613 +++++++++ .../01-queries/_category_.json | 4 + .../02-mutations/01-transaction-mutations.md | 94 ++ .../02-mutations/02-collections-mutations.md | 495 ++++++++ .../02-mutations/03-tokens-mutations.md | 1033 +++++++++++++++ .../02-mutations/04-wallets-mutations.md | 125 ++ .../02-mutations/05-fuel-tanks-mutations.md | 827 ++++++++++++ .../02-mutations/06-marketplace-mutations.md | 349 ++++++ .../02-mutations/07-beam-mutations.md | 130 ++ .../02-mutations/_category_.json | 4 + .../03-api-reference/03-api-reference.md | 93 ++ .../03-api-reference/03-websocket-events.md | 41 + .../04-important-arguments.md | 434 +++++++ .../03-api-reference/_category_.json | 3 + .../01-validator-staking.md | 125 ++ .../02-enjin-relaychain.md | 35 + .../02-stake-exchange-pallet.md | 175 +++ .../02-enjin-relaychain/03-slashing.md | 49 + .../02-enjin-relaychain/_category_.json | 4 + .../01-multitoken-pallet.md | 328 +++++ .../02-fuel-tank-pallet.md | 370 ++++++ .../03-enjin-matrixchain.md | 22 + .../03-marketplace-pallet.md | 151 +++ .../03-enjin-matrixchain/_category_.json | 4 + .../04-enjin-blockchain.md | 26 + .../04-enjin-blockchain/_category_.json | 4 + .../05-enjin-platform/02-chain-state.md | 99 ++ .../03-working-with-events.md | 177 +++ .../02-local-installation.md | 187 +++ .../04-enterprise-on-prem/03-faq.md | 33 + .../04-enterprise-on-prem.md | 87 ++ .../04-laravel-horizon.md | 18 + .../05-extending-the-platform.md | 88 ++ .../_Telemetry_02-local-installation.md | 197 +++ .../04-enterprise-on-prem/_category_.json | 3 + .../05-enjin-platform/05-enjin-platform.md | 107 ++ .../05-enjin-platform/_category_.json | 4 + .../06-enjin-products/01-enjin-tech-stack.md | 31 + .../06-enjin-products/02-enjin-coin.md | 45 + .../06-enjin-products/03-enjin-wallet.md | 24 + .../06-enjin-products/04-nftio-marketplace.md | 26 + .../version-2.x/06-enjin-products/05-beam.md | 68 + .../06-enjin-products/_category_.json | 8 + versioned_sidebars/version-2.x-sidebars.json | 8 + versions.json | 3 + 119 files changed, 27247 insertions(+) create mode 100644 versioned_docs/version-2.x/01-getting-started/01-introduction.md create mode 100644 versioned_docs/version-2.x/01-getting-started/02-quick-start-guide.md create mode 100644 versioned_docs/version-2.x/01-getting-started/04-using-the-enjin-platform.md create mode 100644 versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md create mode 100644 versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/05-using-enjin-api.md create mode 100644 versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/_category_.json create mode 100644 versioned_docs/version-2.x/01-getting-started/06-using-wallet-daemon.md create mode 100644 versioned_docs/version-2.x/01-getting-started/_category_.json create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/01-creating-collections.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/_category_.json create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/_category_.json create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/_category_.json create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/_category_.json create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-example.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/_category_.json create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/03-enforced-rarity.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/04-brand-collab-nfts.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/05-enj-infusion.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/06-the-multiverse.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/_category_.json create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/_dispatch-transactions-using-fuel-tanks.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/01-getting-started.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/02-graphql-requests.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/03-platform-events.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/_category_.json create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/_logger.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/03-implementation-breakdown.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/_category_.json create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/06-enjin-discord-bot.md create mode 100644 versioned_docs/version-2.x/02-guides/01-platform/_category_.json create mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/02-staking-integrtion-guide.md create mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/03-creating-nomination-pools.md create mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/04-joining-nomination-pools.md create mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/_category_.json create mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-enjin-blockchain-nodes.md create mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-running-a-node.md create mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/02-run-relaychain-node.md create mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/03-run-matrixchain-node.md create mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/_category_.json create mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md create mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/02-security-best-practices-for-validators.md create mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/03-how-to-stop-validating.md create mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/_category_.json create mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/_category_.json create mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/03-running-enjin-indexer.md create mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/_category_.json create mode 100644 versioned_docs/version-2.x/02-guides/_category_.json create mode 100644 versioned_docs/version-2.x/03-api-reference/01-queries/01-transactions-queries.md create mode 100644 versioned_docs/version-2.x/03-api-reference/01-queries/02-collections-queries.md create mode 100644 versioned_docs/version-2.x/03-api-reference/01-queries/03-tokens-queries.md create mode 100644 versioned_docs/version-2.x/03-api-reference/01-queries/04-wallets-queries.md create mode 100644 versioned_docs/version-2.x/03-api-reference/01-queries/05-fuel-tank-queries.md create mode 100644 versioned_docs/version-2.x/03-api-reference/01-queries/06-marketplace-queries.md create mode 100644 versioned_docs/version-2.x/03-api-reference/01-queries/07-beam-queries.md create mode 100644 versioned_docs/version-2.x/03-api-reference/01-queries/_category_.json create mode 100644 versioned_docs/version-2.x/03-api-reference/02-mutations/01-transaction-mutations.md create mode 100644 versioned_docs/version-2.x/03-api-reference/02-mutations/02-collections-mutations.md create mode 100644 versioned_docs/version-2.x/03-api-reference/02-mutations/03-tokens-mutations.md create mode 100644 versioned_docs/version-2.x/03-api-reference/02-mutations/04-wallets-mutations.md create mode 100644 versioned_docs/version-2.x/03-api-reference/02-mutations/05-fuel-tanks-mutations.md create mode 100644 versioned_docs/version-2.x/03-api-reference/02-mutations/06-marketplace-mutations.md create mode 100644 versioned_docs/version-2.x/03-api-reference/02-mutations/07-beam-mutations.md create mode 100644 versioned_docs/version-2.x/03-api-reference/02-mutations/_category_.json create mode 100644 versioned_docs/version-2.x/03-api-reference/03-api-reference.md create mode 100644 versioned_docs/version-2.x/03-api-reference/03-websocket-events.md create mode 100644 versioned_docs/version-2.x/03-api-reference/04-important-arguments.md create mode 100644 versioned_docs/version-2.x/03-api-reference/_category_.json create mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/01-validator-staking.md create mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/02-enjin-relaychain.md create mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/02-stake-exchange-pallet.md create mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/03-slashing.md create mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/_category_.json create mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md create mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/02-fuel-tank-pallet.md create mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/03-enjin-matrixchain.md create mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/03-marketplace-pallet.md create mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/_category_.json create mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/04-enjin-blockchain.md create mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/_category_.json create mode 100644 versioned_docs/version-2.x/05-enjin-platform/02-chain-state.md create mode 100644 versioned_docs/version-2.x/05-enjin-platform/03-working-with-events.md create mode 100644 versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/02-local-installation.md create mode 100644 versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/03-faq.md create mode 100644 versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/04-enterprise-on-prem.md create mode 100644 versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/04-laravel-horizon.md create mode 100644 versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/05-extending-the-platform.md create mode 100644 versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/_Telemetry_02-local-installation.md create mode 100644 versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/_category_.json create mode 100644 versioned_docs/version-2.x/05-enjin-platform/05-enjin-platform.md create mode 100644 versioned_docs/version-2.x/05-enjin-platform/_category_.json create mode 100644 versioned_docs/version-2.x/06-enjin-products/01-enjin-tech-stack.md create mode 100644 versioned_docs/version-2.x/06-enjin-products/02-enjin-coin.md create mode 100644 versioned_docs/version-2.x/06-enjin-products/03-enjin-wallet.md create mode 100644 versioned_docs/version-2.x/06-enjin-products/04-nftio-marketplace.md create mode 100644 versioned_docs/version-2.x/06-enjin-products/05-beam.md create mode 100644 versioned_docs/version-2.x/06-enjin-products/_category_.json create mode 100644 versioned_sidebars/version-2.x-sidebars.json create mode 100644 versions.json diff --git a/docusaurus.config.js b/docusaurus.config.js index 278a9dd..c9999ec 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -46,6 +46,19 @@ const config = { routeBasePath: '/', // Serve the docs at the site's root sidebarPath: './sidebars.js', editUrl: 'https://github.com/enjin/docs/tree/master/', + lastVersion: '2.x', + includeCurrentVersion: true, + versions: { + current: { + label: 'v3 (Beta)', + path: 'v3', + banner: 'unreleased', + }, + '2.x': { + label: 'v2', + path: '', + }, + }, }, blog: false, theme: { @@ -91,6 +104,10 @@ const config = { {href: 'https://enjin.io/changelog', label: 'Changelog', position: 'left'}, {href: '/tools', label: 'Tools', position: 'left'}, + { + type: 'docsVersionDropdown', + position: 'right', + }, { href: 'https://console.enjin.io', label: 'Blockchain Console', diff --git a/versioned_docs/version-2.x/01-getting-started/01-introduction.md b/versioned_docs/version-2.x/01-getting-started/01-introduction.md new file mode 100644 index 0000000..0c21548 --- /dev/null +++ b/versioned_docs/version-2.x/01-getting-started/01-introduction.md @@ -0,0 +1,69 @@ +--- +title: "Introduction to the Enjin Ecosystem" +slug: "introduction" +description: "Explore Enjin: the NFT-focused blockchain ecosystem purpose-built for games & apps. Discover its Platform API, SDKs (C#, C++), Fuel Tanks, and Managed Wallets" +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +![Welcome to the Enjin Ecosystem](/img/getting-started/enjin-ecosystem.png) + +Welcome to Enjin! You're stepping into a comprehensive ecosystem designed for building the next generation of games and applications powered by (NFTs). This page provides an overview of what Enjin offers, its key components, and the powerful capabilities you can leverage. + +## What is Enjin? + +Enjin is a pioneering platform engineered to support the dynamic activity, numerous digital items, and high performance demanded by modern games and applications. Its most distinctive feature is the **native integration of s at the 's protocol level**. This foundational design significantly simplifies development, enhances security, and facilitates true digital ownership, making it easier than ever to create, manage, and integrate s. + +## Understanding the Enjin Ecosystem +Enjin is more than just a ; it's a suite of interconnected tools and services designed to work together seamlessly: + +
+ +
+ +- **Enjin Blockchain:** The core of the ecosystem, is a **, dual-layer chain** built for **scalability, security, and ease of use** in game and app development. As a purpose-built for these applications, its protocol-level functionalities optimize creation, management, and transactions, ensuring efficiency and reliability. +*[Learn more about Enjin Blockchain Architecture](/04-enjin-blockchain/04-enjin-blockchain.md)* +- **The :** This platform is your primary gateway to the . Its robust API allows you to manage digital assets, interact with users, and implement features without requiring deep expertise. +*[Explore the API Reference](/03-api-reference/03-api-reference.md)* +- **Software Development Kits (SDKs) & Code Snippets:** To accelerate your development, Enjin offers official **[s](/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md) (primarily in C# and C++)**. You'll also find **code snippets for common actions in , cURL, JavaScript, Node.js, and Python**. These resources include comprehensive documentation and examples to streamline integration. +*[Check out the SDKs page](/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md)* +- **Key Enjin Products:** + - **:** A secure, user-friendly mobile wallet for storing, managing, and trading s and other digital assets from the Enjin ecosystem and beyond. +*[Learn more about the Enjin Wallet](/06-enjin-products/03-enjin-wallet.md)* + - **:** The official marketplace for the Enjin ecosystem, where users can discover, buy, sell, and trade s, fostering a vibrant economy for your digital assets. + *[Visit NFT.io](https://nft.io)* + - **:** A powerful marketing tool for distributing s in bulk via QR codes, links, or other methods—ideal for airdrops, rewards, and promotional campaigns. + *[Discover Enjin Beam](/06-enjin-products/05-beam.md)* + +## What You Can Achieve with Enjin + +Enjin empowers you to build innovative experiences by leveraging its unique features: + +1. **Create and Manage Digital Assets Securely:** + Easily design, , and oversee your s and individual s. The 's native support for s ensures inherent security, efficiency, and standardized operations for all your digital assets. +2. **Seamlessly Integrate NFTs into Games and Apps:** + Use the API and s to effortlessly embed rich functionalities. tokens on-demand, program asset transfers, implement freezing mechanics, retrieve holder information, and execute a wide array of interactions with ease. +3. **Simplify User Onboarding with Managed Wallets:** + Lower the entry barrier for users new to cryptocurrency by managing their wallets via the . Users can enjoy seamless interactions and later choose to export their assets to their self-custody , significantly streamlining the Web2-to-Web3 transition. + *[Read about Managed Wallets](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md)* +
+ +
+4. **Enhance User Experience with Fuel Tanks:** + Improve the experience for users, especially those new to Web3, by subsidizing using s. Create and fund on-chain pools with dynamic eligibility rules to abstract away fee complexities, making interactions smoother. + *[Read about Fuel Tanks](/04-enjin-blockchain/03-enjin-matrixchain/02-fuel-tank-pallet.md)* + +## Enjin Ecosystem Slack + +Join the official [Enjin Ecosystem Slack community]( +https://enj.in/ecosystem-slack) to connect with other developers and community members. It's a great place to: + +- Ask questions and get help +- Share your projects and ideas +- Interact directly with Atlas Development, a core contributor to Enjin + +## Ready to Start Building? + +This overview offers a glimpse into the Enjin ecosystem. To get hands-on and begin your development journey, our Quick Start Guide is the perfect next step. + +➡️ **[Proceed to the Quick Start Guide](/01-getting-started/02-quick-start-guide.md)** \ No newline at end of file diff --git a/versioned_docs/version-2.x/01-getting-started/02-quick-start-guide.md b/versioned_docs/version-2.x/01-getting-started/02-quick-start-guide.md new file mode 100644 index 0000000..dca7742 --- /dev/null +++ b/versioned_docs/version-2.x/01-getting-started/02-quick-start-guide.md @@ -0,0 +1,289 @@ +--- +title: "Quick Start Guide" +slug: "quick-start-guide" +description: "Begin your journey with Enjin's blockchain ecosystem using our Quick Start Guide. Learn how to integrate blockchain items into your projects seamlessly" +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +# Enjin Quick Start Guide + +Welcome! This guide provides the essential steps to quickly integrate the and s into your game or application using the . We'll use the testnet for a safe and free development environment. + +:::info **By the end of this guide, you will have:** + +- Set up your developer account on the Canary . +- Connected your developer wallet to authorize actions. +- Created your first blockchain and . +- Learned how to link your users' s to your application. + - Sent a token to a linked user wallet. + - Read tokens from a linked user wallet +::: + +## 1. Create Your Enjin Canary Platform Account + +### What is the Enjin Platform? + +The Enjin Platform is your central hub for managing blockchain projects, collections, and digital items (NFTs and Fungible Tokens). We'll start by creating an account on the Canary Platform, which operates on Enjin's testnet blockchain. This allows you to experiment without using real funds or affecting the main network. + +### Steps to Create an Account: + +1. Navigate to the Canary Platform website: https://platform.canary.enjin.io/ +2. Click on the "Create an account" button. +3. Fill in your account credentials and click on the "Register" button. +4. Verify your email address by clicking the link sent to your inbox. +5. Log in to your newly created Canary Platform account. + +## 2. Connect Your Wallet Account + +### Why Connect a Wallet? + +To perform actions on the blockchain (like creating collections or minting tokens) through the Platform, you need to authorize these actions. Connecting your developer wallet (like the Enjin Wallet app) allows the Platform to request your signature for transactions you initiate. + +### Steps to Connect Your Wallet: + +To connect your wallet, click on the "Connect Wallet" button in the top right corner and follow the on-screen instructions. + +![Connecting a wallet app on the Enjin Platform](/img/getting-started/connect-wallet-app.gif) + +:::tip Automating Signatures with the +For applications requiring automated actions (like minting rewards in real-time or batch transferring items without manual confirmation each time), you can set up the **Enjin Wallet Daemon**. This secure service runs on your server and can automatically sign transaction requests sent to your Enjin Platform. + +**Learn More:** [Set up the Enjin Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) +::: + +## 3. Create Your First Collection and Token + +### What are Collections and Tokens? +Let's define the core building blocks for your digital items on the : + +- **Collection:** Think of a as a container or category for your digital items. It helps organize items related to your specific game, application, or a particular series. All items within a collection share certain base properties defined when you create the collection. +- **Token (`multiToken`):** A **Token** represents the actual digital item that users can own within a Collection. On the Enjin Blockchain, all tokens fundamentally exist as **s**. This is a core feature built directly into the blockchain protocol itself. + +:::tip Structuring Your Project +A common approach is to create: +- **One Collection** for your entire game or application. +- **Multiple Tokens** within that collection, representing the various items, characters, achievements, or currencies your users can own. +::: + +### Steps for creating a collection and token: + +:::info **Ways to Create Blockchain Requests:** +You can interact with the Enjin Platform and initiate blockchain requests in several ways: +- **Manually:** Using the user interface of the Enjin Platform Cloud. +- **Programmatically:** + - Sending **direct HTTP requests** to the [Enjin GraphQL API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md). + - Using the [**official Enjin SDKs**](/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md): + - [C# SDK](https://github.com/enjin/platform-csharp-sdk) + - [C++ SDK](https://github.com/enjin/platform-cpp-sdk) + - **Other Languages:** The [Guides section](/category/integration-guides) provides code snippets for GraphQL, cURL, JavaScript, Node.JS, and Python for most operations. + - **AI Assistant:** Utilize the [Enjin Platform AI assistant](https://chatgpt.com/g/g-678f70643d2c8191a78baff699e46e5f-enjin-platform-ai) to help construct API calls. +::: + +1. **Create Collection:** + - In the Platform menu, navigate to the **[Collections page](https://platform.canary.enjin.io/collections)**. Then, click on the "**[Create Collection](https://platform.canary.enjin.io/create/collection)**" button. + ![Creating a collection](/img/guides/managing-tokens/create-collection.gif) + - Customize your collection - you can specify the collection's name, description, media, attributes, Mint and Market policies, and more. + To learn more about collection configuration, please check the [Create Collections guide](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md). + - Submit the creation request by clicking on the **Create** button. This will create a signature request which you can view in the [Transactions page](https://platform.canary.enjin.io/transactions), and will require a transaction to be signed. + ![](/img/guides/managing-tokens/pending-create-collection-txn.png) + - To sign the Create Collection transaction request - Click on the **"Sign"** button and approve the signature request with your connected wallet. + - Once the transaction is confirmed on-chain, the new collection will appear in your [Collections page](https://platform.canary.enjin.io/collections), where you can also find its ID, keep note of it for the next step. +2. **Create Token:** + - In the Platform menu, navigate to the **[Tokens page](https://platform.canary.enjin.io/tokens)**. Then, click on the "**[Create Token](https://platform.canary.enjin.io/create/token)**" button. + - Customize your token - you can specify the token's name, description, media, attributes, and more. + To learn more about token configuration, please check the [Create Tokens guide](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md). + - In the **"Collection ID"** input field, insert the collection id of the collection you just created. You may also click the input field and select the id from the dropdown menu. + - In the **"Recipient"** input field, insert the of the wallet that you wish to receive the token's initial supply. You can find the address of your connected wallet in the [settings page](https://platform.canary.enjin.io/settings) by clicking on the Settings icon in the top right corner, followed by the "Settings" button. + - Submit the creation request by clicking on the **Create** button. This requires another transaction, which you can sign in the [Transactions page](https://platform.canary.enjin.io/transactions). + +:::tip Programmatic Creation +You can also create collections and tokens using the API or SDKs. + +➡️ **See Guides:** +- [Guide: Create Collections Programmatically](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md#option-b-using-the-enjin-api--sdks) +- [Guide: Create Tokens Programmatically](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md#option-b-using-the-enjin-api--sdks) +::: + +## 4. Link a User's Wallet to Your Application + +### Why Link User Wallets? + +To interact with a specific user's blockchain items (check their holdings, send them items), you need to know their public wallet address and have proof they control it. This process involves **verifying** the user's ownership of their wallet and then **associating** that verified wallet address with their account within your application's database. This association is what allows you to connect their in-app identity with their blockchain identity. + +The typical flow for this is: + +1. Your application requests a unique wallet verification QR code from the Enjin API. +2. Your application displays this QR code to the user. +3. The user scans the QR code with their Enjin Wallet app. +4. The wallet confirms ownership, securely providing your application with the user's public wallet address. +5. Your application then saves this verified wallet address and **associates** it with the user's internal ID in your database. + +Once this association (or "link") is established, you can: + + - Grant in-game perks based on NFT ownership. + - Airdrop or transfer items directly to the user's associated wallet. + - Display their relevant blockchain inventory within your app. + +### Implementation (GraphQL Example): + +To request a wallet verification code, we need to interact with the **** +The Enjin Platform API is a API. If you are unfamiliar with GraphQL, we recommend checking out the [How to Use GraphQL](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md) guide. + +:::info Quick Start: Sending API Requests with the +In this example, we will be using the to easily send an API request from within the browser. +In a production-ready integration, your app will typically send API calls using HTTP requests or one of the [official Enjin SDKs](/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md). You can learn more about this in the [Using the Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) page. +::: + +To interact with the Enjin Platform API, head over to the [Enjin Platform](https://platform.canary.enjin.io/), click on the settings icon in the top right corner, followed by **"platform-core"** in the available packages. This will navigate to the [Enjin Platform GraphiQL Playground](https://platform.canary.enjin.io/graphiql/). + +Once you are in the GraphiQL playground page, copy & paste the following query: + +```graphql +query RequestAccount{ + RequestAccount{ + qrCode #Returns a QR code for your user to scan + verificationId #An id to this account request call. Save this for next step + } +} +``` + +This will return a link to a QR Code for you to display to your user, and a unique verification id to track the request. +In your application, you'll display this QR code to your user and instruct them to scan it with their . + +To confirm that the user scanned the QR code and approved the request, copy & paste the following query (replace the `replace_verification_id_here` placeholder with the `verificationId` from the previous call's response): + +```graphql +query GetAccountVerified{ + GetAccountVerified(verificationId: "replace_verification_id_here"){ + verified + account{ + publicKey + address + } + } +} +``` + +If the user successfully scanned the QR code and approved the request, the response will show `"verified": true` and contain the user's wallet address: + +```json +{ + "data": { + "GetAccountVerified": { + "verified": true, + "account": { + "publicKey": "0x5a6aae294416f3e875d9a8975658905002cfd3e5e64105d76296c4b0adbfd77e", //The public key of the verified account + "address": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //The address of the verified account + } + } + } +} +``` + +At this point, your application should take the `address` value from the response and store it in your user database, associated with the specific user who scanned the QR code. This is the crucial step that "links" the user's in-app identity with their blockchain wallet address. + +:::tip Learn more about linking user wallets +Check the full in-depth guide: [Linking User Wallets](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md). +::: + +## 5. Send a Token to a User + +### Why Send Tokens? + +This is a core interaction – transferring an NFT from your project's wallet (or another source) to a user's linked wallet. This could be for: +- Rewarding achievements. +- In-app purchases. +- Airdrops or giveaways. + +### Implementation (GraphQL Example): + +Sending tokens involves creating a transfer request via the API. You'll need the Token ID, the recipient's linked wallet address, and the amount (for s). + +Copy & paste the following mutation into the GraphiQL playground: + +```graphql +mutation TransferToken{ + SimpleTransferToken( + collectionId: 36105 #Specify the collection ID here + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the recipent address here + params: { + tokenId: {integer: 0} #Specify the token ID + amount: 1 #Choose the transfer amount + } + ){ + id + method + state + } +} +``` + +:::note This request needs to be signed. +You can sign it in the [Transactions page](https://platform.canary.enjin.io/transactions), as we did in the previous steps. +::: + +:::tip Learn more about sending tokens +Check the full in-depth guide: [Sending Tokens](/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md). +::: + +## 6. Reading Linked User's Tokens + +### Why Read User Tokens? + +Once a user has linked their wallet (as shown in [Step 4](#4-link-a-users-wallet-to-your-application)), you'll often need to check which tokens they own within your collection(s). This allows you to: + +- Display their relevant inventory within your game or application interface (like in-game currency or an item). +- Verify ownership of specific NFTs to grant access, perks, or abilities. +- Trigger game logic based on the items they hold. + +### Implementation (GraphQL Example): + +To read the user's wallet, copy & paste the following query into the GraphiQL playground, using the user's linked wallet address: + +```graphql +query FetchingWalletTokens{ + GetWallet(account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"){ #Specify the account address + tokenAccounts{ + edges{ + node{ + balance + token{ + tokenId + collection{ + collectionId + } + attributes{ + key + value + } + } + } + } + } + } +} +``` + +:::tip Learn more about reading user wallet balances +Check the full in-depth guide: [Reading User Wallet Balances](/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md). +::: + +## 7. Next Steps and Advanced Features + +Congratulations! You've covered the basics of setting up your project on the Enjin Platform, creating items, and enabling user wallet interactions. + +:::warning **Ready for Production?** +Remember, this guide used the **Canary testnet** for development and testing. When you are ready to launch your application live, you will need to: +1. Set up your project on the **mainnet Enjin Platform Cloud**: https://platform.enjin.io/ +2. Update your application's API calls or SDKs to use the [**mainnet GraphQL API endpoints**](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md#mainnet) +::: + +Now you can explore more advanced integrations: + +- **On-Demand Minting:** Programmatically create *new* tokens directly into users' wallets based on real-time application events (e.g., crafting an item, completing a quest). This avoids pre-minting large supplies. + ➡️ [Learn about On-Demand Minting](/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md#option-b-using-the-enjin-api--sdks) +- **Enjin Wallet Daemon:** If your application requires frequent, automated minting or transferring without manual intervention, setting up the Wallet Daemon is highly recommended for secure signature automation. + ➡️ [Set up the Enjin Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) +- **Managed Wallets:** Simplify onboarding for users unfamiliar with crypto wallets. You can create and manage wallets for your users within the platform. Users can interact with blockchain features seamlessly, and later choose to export their items to their own self-custody Enjin Wallet if they wish. This significantly lowers the barrier to entry for Web2 users. + ➡️ [Explore Managed Wallets](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) \ No newline at end of file diff --git a/versioned_docs/version-2.x/01-getting-started/04-using-the-enjin-platform.md b/versioned_docs/version-2.x/01-getting-started/04-using-the-enjin-platform.md new file mode 100644 index 0000000..a4fae39 --- /dev/null +++ b/versioned_docs/version-2.x/01-getting-started/04-using-the-enjin-platform.md @@ -0,0 +1,589 @@ +--- +title: "Using the Enjin Platform" +slug: "using-the-enjin-platform" +description: "Discover how the Enjin Platform simplifies blockchain asset management for developers, enabling seamless integration of tokenized assets in games and applications." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info What you'll need: +Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) to pay for and for s. +You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +::: + +## 1. Set Up an Account + +To get started, go ahead and create an account on the and **verify your email address.** In this tutorial, we'll be using the [Enjin Platform Cloud (Testnet)](https://platform.canary.enjin.io). + +:::info There are three versions of the Enjin Platform to choose from: +- The [Enjin Platform Cloud (Testnet)](https://platform.canary.enjin.io), connected to the Enjin . +- The [Enjin Platform Cloud (Mainnet)](http://platform.enjin.io/), connected to the Enjin . +- The [Enterprise On-Prem Enjin Platform](/05-enjin-platform/04-enterprise-on-prem/04-enterprise-on-prem.md) which you can run locally to connect to either network. +::: + +### Things you should know + +- is , which is why we recommend building your initial proof-of-concept on Enjin – the fast and free environment for testing Enjin's tools. +- Every "" request (i.e, minting a ) must be signed by a . + +## 2. Set Up an API Token + +Once your account is created, create a new API token by clicking the "**Create API Token**", which can be found on the [Account Settings](https://platform.canary.enjin.io/settings) page. + +![Creating an API token on the Enjin Platform](/img/getting-started/create-api-token.gif) + +Copy and save that token somewhere safe, you'll need that to automatically approve transactions using a . + +:::tip Note +You can always revoke the token and create another one in the future. +::: + +## 3. Set Up a Wallet + +Transactions can be approved in 2 different ways: + +- **Manually:** Using the [Enjin Wallet](https://enjin.io/products/wallet) / [Polkadot.js](https://polkadot.js.org/). +- **Automatically:** Using the [Enjin Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md). + +:::tip Start by manually approving transactions +If you're using the Enjin Platform for the first time, we recommend [Using Enjin Wallet / Polkadot.js](#a-using-enjin-wallet--polkadotjs) +Enjin wallet app is available on both [iOS](https://enj.in/ios-wallet) and [Android](https://enj.in/android-wallet). +::: + +### A. Using Enjin Wallet / Polkadot.js + +Connect your wallet by clicking the "**Connect Wallet**" button in the top right corner and follow the on-screen instructions. +The also offers gamers a user-friendly, secure, and rewarding experience, ensuring their continuous engagement with your game. + +![Connecting a wallet app on the Enjin Platform](/img/getting-started/connect-wallet-app.gif) + +### B. Using Daemon Wallet + +The Enjin Wallet Daemon can automatically sign requests for you. + +To download and set up wallet daemon, head over to the [Wallet Daemon Setup](/01-getting-started/06-using-wallet-daemon.md) page. +To ensure that only authorized requests are approved, the Wallet daemon needs to be configured with an Enjin Platform API Token. +Create an API Token on the [Account Settings](https://platform.canary.enjin.io/settings) page and set the Platform API Token in your Wallet Daemon configuration. + +Once your wallet daemon is connected, your Platform account is set up and ready to use! + +## 4. Create Platform Requests + +Now that the Platform account is set up, we can start creating platform requests. +In this example we will be creating a . + +**There are two ways to create Platform requests:** + +- [Using the Platform User Interface](#option-a-using-the-enjin-dashboard) +- [Using the GraphQL API](#option-b-using-the-enjin-api--sdks) + +:::info Which approach should I use? +Everything that can be done via the Platform's User Interface, can be done programmatically via the API or any of the Platform s. +If you are just starting out, we recommend using the Platform User Interface as it's more user friendly. +However, if you need to make a Platform request programmatically, you can do that via the / s. +::: + +### Option A. Using the Enjin Dashboard + +In the Platform menu, navigate to "**[Collections](https://platform.canary.enjin.io/collections)**". Then, click the "**[Create Collection](https://platform.canary.enjin.io/create/collection)**" button. + +![Create collection form on Enjin Platform](/img/getting-started/create-collection-form.png) + +From here, you can customize your collection's Mint Policy, Market Policy, Explicit Royalty Currencies (optional), and Attributes. + +Once you're satisfied with the options, click on the "**Create**" button at the bottom right corner to create the request. + +![Collection created banner on Enjin Platform](/img/getting-started/collection-created-banner.png) + +### Option B. Using the Enjin API & SDKs + +To create a Platform request programmatically, use the API, or one of the s. +This mutation will set up a new transaction that once finalized on-chain will contain the new collection id in the transaction . + +:::tip Test your requests in the GraphiQL Playground: +- [Testnet](https://platform.canary.enjin.io/graphiql) +- [Mainnet](https://platform.enjin.io/graphiql) +::: + +:::tip +If you are not using the playground, you need to add your API token to the headers to be authenticated, like so: +```json +"Authorization": "" +``` +::: + + + +```graphql +mutation CreateCollection { + CreateCollection(mintPolicy: { forceCollapsingSupply: false }) #Set to true to enforce collapsing supply mint policy + { + id + method + state + } +} +``` + + +```json +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation Infuse(\r\n $collection_id: BigInt!\r\n $token_id: EncodableTokenIdInput!\r\n $amount: BigInt!\r\n) {\r\n Infuse(collectionId: $collection_id, tokenId: $token_id, amount: $amount) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"forceCollapsingSupply":false}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Setup the mutation +var createCollection = new CreateCollection() + .SetMintPolicy(new MintPolicy().SetForceCollapsingSupply(false)); //Set to true to enforce collapsing supply mint policy + +// Define and assign the return data fragment to the mutation +var transactionFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +createCollection.Fragment(transactionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendCreateCollection(createCollection); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cplusplus +#include "EnjinPlatformSdk/CoreMutations.hpp" +#include +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Setup mutation data + shared_ptr mintPolicy = make_shared(); + mintPolicy->SetForceSingleMint(make_shared(false)); + + // Setup mutation + CreateCollection createCollection = CreateCollection(); + createCollection.SetMintPolicy(mintPolicy); + + // Define and assign the return data fragment to the mutation + shared_ptr transactionFragment = make_shared(); + transactionFragment + ->WithId() + .WithMethod() + .WithState(); + + createCollection.SetFragment(transactionFragment); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>>> futureResponse = SendCreateCollection(*client, createCollection); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& transaction = gqlResult->GetData()->GetResult(); + + std::cout << to_string(transaction->GetId().value()) << std::endl; + std::cout << ToString(transaction->GetMethod().value()) << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation CreateCollection($forceCollapsingSupply: Boolean) { + CreateCollection( + mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } + ) { + id + method + state + } + } + `, + variables: { + forceCollapsingSupply: false //Set to true to enforce collapsing supply mint policy + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation CreateCollection($forceCollapsingSupply: Boolean) { + CreateCollection( + mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } + ) { + id + method + state + } + } + `, + variables: { + forceCollapsingSupply: false //Set to true to enforce collapsing supply mint policy + } +}, { + headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation CreateCollection($forceCollapsingSupply: Boolean) { + CreateCollection( + mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } + ) { + id + method + state + } +} +''' + +variables = {'forceCollapsingSupply': False} #Set to true to enforce collapsing supply mint policy + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +A WebSocket event will also be fired so you can pick up the collection in real time by listening to the app channel on the WebSocket. + +## 5. Approve Platform Requests + +Once a Platform request is created, all that remains is to approve it, which will sign the transaction and broadcast it to the Enjin Blockchain + +Transaction requests are displayed in the "**[Transactions](https://platform.canary.enjin.io/transactions)**" menu. + +![Pending create collection txn on Enjin Platform](/img/getting-started/pending-create-collection-txn.png) + + +Since this transaction is a , you will need to sign the transaction using your Wallet. + +- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. +- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. + +![Approving the create collection txn on Enjin Platform](/img/getting-started/approving-create-collection-txn.gif) + +And that's it! The transaction is then broadcasted to the Enjin Blockchain, and once approved, it will be included in a . + +## 6. Receive Transaction Information + +There are 3 ways to receive the transaction status and information: + +- [Receive Transaction Information Using the Platform User Interface](#receive-transaction-information-using-the-platform-user-interface) +- [Receive Transaction Information Using the Enjin API / SDKs](#receive-transaction-information-using-the-enjin-api--sdks) +- [Receive Transaction Information by Listening to Platform Websocket Events](#receive-transaction-information-by-listening-to-platform-websocket-events) + +### Receive Transaction Information Using the Platform User Interface + +To check the transaction status, head over to the [Transactions](https://platform.canary.enjin.io/transactions) page locate the transaction and click on the button to it's right to show more details. +Once the transactions approves on-chain, the transaction will get updated with the `FINALIZED` state and the transaction events will be displayed. +In the events, we can find useful information, such as the newly created collection ID. + +![How to find your newly created collection ID](/img/getting-started/find-newly-created-collection-id.gif) + +### Receive Transaction Information Using the Enjin API & SDKs + +To receive the transaction information programmatically, You can use the `GetTransaction` query and set the `id` to the ID received in the `CreateCollection` mutation. +But for this example, we're going to use the `GetPendingEvents` query, which returns all of the events emitted to your platform account. + +:::info This query might return items in multiple pages using Connections +To learn how to use GraphQL cursors for pagination, head to [Using the API --> Pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination). +::: + +**Query:** + + + +```graphql +query GetPendingEvents{ + GetPendingEvents{ + edges{ + node{ + name + data + } + } + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query": "query GetPendingEvents { GetPendingEvents { edges { node { name data } } } }"}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +var filterInputArray = new StringFilterInput[] +{ + new StringFilterInput() + .SetType(FilterType.And) + .SetFilter("collection;2000"), +}; + +// Setup the query +var getPendingEvents = new GetPendingEvents() + .SetChannelFilters(filterInputArray); + +// Define and assign the return data fragment to the query +var pendingEventsFragment = new ConnectionFragment() + .WithEdges(new EdgeFragment() + .WithNode(new PendingEventFragment() + .WithId() + .WithUuid() + .WithName() + .WithSent() + .WithChannels() + .WithData() + ) + ); + +getPendingEvents.Fragment(pendingEventsFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendGetPendingEvents(getPendingEvents); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cplusplus + +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + query GetPendingEvents{ + GetPendingEvents{ + edges{ + node{ + name + data + } + } + } + } + `, + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + query GetPendingEvents{ + GetPendingEvents{ + edges{ + node{ + name + data + } + } + } + } + `, +}, { + headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' + query GetPendingEvents{ + GetPendingEvents{ + edges{ + node{ + name + data + } + } + } + } +''' + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +**Response:** + +```json +{ + "data": { + "GetPendingEvents": { + "edges": [ + { + "node": { + "name": "platform:transaction-created", + "data": { + "id": 1648, + "state": "PENDING", + "method": "CreateCollection", + "idempotencyKey": "00e32324-acfa-4ddb-8b7d-6dd8d0b4f694" + } + } + }, + { + "node": { + "name": "platform:transaction-updated", + "data": { + "id": 1648, + "state": "BROADCAST", + "method": "CreateCollection", + "result": null, + "transactionId": null, + "idempotencyKey": "00e32324-acfa-4ddb-8b7d-6dd8d0b4f694", + "transactionHash": "0xeb2510094c53317e19ca188b5ba8a21b81f36f4bc1be09e079b46ee7b9bce754" + } + } + }, + { + "node": { + "name": "platform:collection-created", + "data": { + "owner": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", + "collectionId": "38224", + "idempotencyKey": "00e32324-acfa-4ddb-8b7d-6dd8d0b4f694" + } + } + }, + { + "node": { + "name": "platform:transaction-updated", + "data": { + "id": 1648, + "state": "FINALIZED", + "method": "CreateCollection", + "result": "EXTRINSIC_SUCCESS", + "transactionId": "1404095-2", + "idempotencyKey": "00e32324-acfa-4ddb-8b7d-6dd8d0b4f694", + "transactionHash": "0xeb2510094c53317e19ca188b5ba8a21b81f36f4bc1be09e079b46ee7b9bce754" + } + ] + } + } +} +``` + +The first event with `PENDING` state was emitted when the `CreateCollection` platform request was created. +The second event with `BROADCAST` state was emitted when the transaction was signed and broadcasted to the Enjin Blockchain. +The third event was emitted when the collection was created on-chain, and has the `collectionId` in it's data. +The forth and final event with `FINALIZED` state was emitted when the platform request was finalized. + +:::info Note +For the full list of platform states, check the [Enjin API --> Important Arguments](/03-api-reference/04-important-arguments.md#state) page. +::: + +:::tip Acknowledging Events +To remove the pending events from the queue after fetching them, set the `acknowledgeEvents` parameter to true +::: + +### Receive Transaction Information by Listening to Platform Websocket Events + +You can also subscribe for events via Websocket. for more information, check out the [Websocket Events](/05-enjin-platform/03-working-with-events.md) + +:::tip What's Next? +Learn how to [Use the API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md). +::: diff --git a/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md b/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md new file mode 100644 index 0000000..39619d2 --- /dev/null +++ b/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md @@ -0,0 +1,792 @@ +--- +title: "How to Use GraphQL" +slug: "../how-to-use-graphql" +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide is intended for users who are new to GraphQL. If you're already familiar with how GraphQL works - how to structure queries, use variables, and interact with GraphQL endpoints—you can skip this page and proceed directly to guides. + +For everyone else, this page will walk you through the essentials of working with GraphQL so you can confidently interact with our API. + +## Understanding GraphQL vs. RESTful API Calls + +To illustrate the differences between GraphQL and RESTful API calls, let's consider an example of creating a token. + +### RESTful API Example + +In a typical RESTful API, you might have an endpoint like `/createToken` with a POST request that includes the necessary data in the request body. This might look something like: + +```http +POST https://platform.canary.enjin.io/api/createToken +Content-Type: application/json + +{ + "recipient": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", + "collectionId": 2406, + "tokenId": 0, + "initialSupply": 1, + "cap": "INFINITE" +} +``` + +And would traditionally return all of the available data related to the call, like so: + +```json +{ + "data": { + "CreateToken": { + "id": "14060", + "transactionId": "null", + "transactionHash": "null", + "method": "CreateToken", + "state": "PENDING", + "encodedData": "...", + "wallet": { + "account": { + "publicKey": "...", + "address": "..." + } + } + } + } +} +``` + +### GraphQL API Example + +In contrast, GraphQL uses a single endpoint for all operations. Instead of defining the operation in the URL and HTTP method, the desired action (query or mutation), its parameters, and the response fields are specified in the request payload. + +In GraphQL: + +- `Queries` are used to retrieve data, similar to a `GET` request in REST. +- `Mutations` are used to create or modify data, similar to `POST`, `PUT`, or `DELETE` requests in REST." + +Here's how the same RESTful request would like like in GraphQL, sent as an HTTP post request to the Canary Platform Cloud GraphQL endpoint https://platform.canary.enjin.io/graphql: + +```graphql +mutation CreateToken { + CreateToken( + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", + collectionId: 2406, + params: { + tokenId: { integer: 0 }, + initialSupply: 1, + cap: { type: INFINITE } + } + ) { + id + method + state + } +} +``` + +With GraphQL, the client controls what data is returned by explicitly specifying the fields in the query (In this example - the `id`, `method`, and `state`). This enables fine-grained control over the response, minimizing unnecessary data transfer. + +GraphQL's ability to query multiple resources in a single request, its strong type system, and its flexibility in querying data make it a powerful tool for interacting with the Enjin Blockchain, providing a seamless experience for developers building with NFTs. + +## GraphiQL Playground + +The serves three purposes: + +1. **Explore** available operations. +2. **Write** operations using an intuitive interface. +3. **View** responses and test your operations before coding them. + +![A breakdown of the GraphiQL Playground](/img/getting-started/graphiql-explorer-breakdown.png) + +### Step 1: Open GraphiQL Playground + +Choose between [Testnet](https://platform.canary.enjin.io/graphiql) or [Mainnet](https://platform.enjin.io/graphiql). + +Explore "[Core Operations](https://platform.canary.enjin.io/graphiql)" for core operations like managing tokens, wallets, users, and transactions. + +:::info GraphiQL Playground: +Please be aware that the provided links below are specifically for the GraphiQL Playground, which is a graphical user interface designed for exploring and interacting with the Enjin Platform GraphQL APIs directly from a web browser. +If you wish to execute GraphQL requests programmatically from your local environment or a cloud-based application, you will need to utilize the actual GraphQL endpoint URLs. These can be found [here](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md#overview-of-graphql-endpoints) +#### Testnet: +- [Core Operations](https://platform.canary.enjin.io/graphiql) +- [Marketplace](https://platform.canary.enjin.io/graphiql/marketplace) +- [Beam](https://platform.canary.enjin.io/graphiql/beam) +- [Fuel Tanks](https://platform.canary.enjin.io/graphiql/fuel-tanks) +#### Mainnet: +- [Core Operations](https://platform.enjin.io/graphiql) +- [Marketplace](https://platform.enjin.io/graphiql/marketplace) +- [Beam](https://platform.enjin.io/graphiql/beam) +- [Fuel Tanks](https://platform.enjin.io/graphiql/fuel-tanks) +::: + +### Step 2: Open the explorer + +Click the `Show GraphQL Explorer` button. + +Here, you can view a complete list of available queries and mutations, along with the associated fields and arguments for each of them. + +

+ Example of using the graphiql playground query builder +

+ +### Step3: Add a query or mutation to start building your operation + +Add a `query` or `mutation` into the "write" panel, and you'll see the list of available options in the explorer update to suit the operation type. + +

+ Example of switching to mutation in the graphiql playground query builder +

+ +### Step 4: Select which operations and datapoints you require + +Click the required data points to automatically add them to the write column. + +This feature eliminates any guesswork when structuring your operations and helps you make the most of the Enjin Platform Schema. + +

+ Example of building a mutation in the graphiql playground query builder +

+ +### Step 5: Click the play button to execute your operation + +To execute your operation and view the response, simply click the `Execute Query` button. + +You can execute any on-chain operation here. You can even mint tokens and view wallet data on [Mainnet](https://platform.enjin.io/graphiql). + +

+ Executing a query in the graphiql playground query builder +

+ +## GraphQL Operation Structures + +The Enjin API features an intricately crafted that is finely tuned to empower the development of scalable and high-speed blockchain-based games, ensuring an immersive experience for players. + +To interface with it directly, you will need to understand how GraphQL operations are structured. + +### Operation Name + +The Operation Name such as `GetTokenInfo` is a user-defined identifier for a specific operation. + +It allows you to organize and distinguish different operations within a single request, making it easier to handle responses and manage complex queries or mutations efficiently. + + + +```graphql +query GetTokenInfo { #'GetTokenInfo' is the Operation Name that distinguishes this operation from others + GetToken( + collectionId: 7153 + tokenId: { integer: 10 } + ) { + supply + } +} +``` + + +```json +{ + "data": { + "GetToken": { + "supply": "1000" + } + } +} +``` + + + +### Fields + +Fields serve as a means to precisely specify the data you want to retrieve in the response of an operation. + +Fields enable you to fine-tune the granularity of data retrieval, ensuring you receive precisely what you need. + +When crafting Queries and Mutations, the Enjin Platform defines what fields are available for you to request. + +For instance, when using the `GetToken` query, you can request the `supply` field, which corresponds to a specific piece of information stored on the blockchain. + + + +```graphql +query { + GetToken( + collectionId: 7153 + tokenId: { integer: 10 } + ) { + supply # supply is a Field that returns the token's supply + } +} +``` + + +```json +{ + "data": { + "GetToken": { + "supply": "1000" + } + } +} +``` + + + +### Arguments + +You can pass data into an operation by using arguments. + +Most Queries and Mutations define a set of input data you can use, for example in the `GetToken` query you can pass in a `collectionId` and a `tokenId` argument. + + + +```graphql +query { + GetToken( + collectionId: 7153 #collectionId is an argument defining which collection to pull data from + tokenId: { integer: 10 } #tokenId is an argument defining which token to pull data from + ) { + supply + } +} +``` + + +```json +{ + "data": { + "GetToken": { + "supply": "1000" + } + } +} +``` + + + +Sometimes arguments will be required, sometimes they will be optional, and some operations may not take any arguments at all. + +The API will tell you if you try to run an operation without a required argument. + +### Aliases + +The alias, such as `TokenInfo` and `CurrentSupply` in the queries provided below, serve as a custom labels for specific fields within the operation. + +Aliases allow you to rename the field in the response, making the data more intuitive and organized, especially when dealing with multiple fields or nested queries within a GraphQL request. + +#### Operation Alias: + +Usually used in batch operations, it allows you to provide a clear and descriptive name for each operations. + +You might use it to distinguish between multiple similar queries or to provide context for the purpose of the query. + + + +```graphql +query GetTokenInfo { + TokenInfo: GetToken( #TokenInfo is the ALIAS that labels the field + collectionId: 7153 + tokenId: { integer: 10 } + ) { + supply + } +} +``` + + +```json +{ + "data": { + "TokenInfo": { + "supply": "1000" + } + } +} +``` + + + +#### Field Alias: + +This option is useful when you want to label a specific field within the response. + +It allows you to rename individual fields in the response for clarity or to avoid naming conflicts. + + + +```graphql +query GetTokenInfo { + GetToken( + collectionId: 7153 + tokenId: { integer: 10 } + ) { + CurrentSupply: supply + } +} +``` + + +```json +{ + "data": { + "GetToken": { + "CurrentSupply": "1000" + } + } +} +``` + + + +### Variables + +Various code blocks in the Enjin Documentation use variables instead of hardcoding arguments. This allows the dynamic insertion of data as you run mutations and queries. + +For example, when a player picks up an item, your game needs to fetch the token information using the token's ID. To dynamically insert the token ID to the query, we should use a variable. + +That way, we will have one "template" for token fetching query, and the game will decide which token to fetch based on the player actions. + +To work with variables in GraphQL, we first need to declare the `$variableName`, as the variables accepted by the operation. Our operations has two arguments - Collection ID and Token ID, so we'll name the variables `$collection_id` and `$token_id` respectively. Both arguments are of type `BigInt`, so the variable declaration will look like that: + +``` +query GetTokenInfo($collection_id: BigInt! $token_id: BigInt) { +``` + +:::tip +Note the exclamation mark (!) after the first variable declaration type BigInt. This represents a _non-nullable_ variable (essentially a _required field_). +::: +We also need to replace the static value in the operation with the `$variableName` + +``` +GetToken( + collectionId: $collection_id //Collection ID 7153 was hardcoded here + tokenId: { integer: $token_id } //Token ID 10 was hardcoded here +) +``` + +And the final step is to pass `variableName: value` in a separate, transport-specific (usually JSON) variables dictionary: + +```json +{ + "collection_id": 7153, + "token_id": 10 +} +``` + +Here's the final operation code: + +**Query:** + +```graphql +query GetTokenInfo($collection_id: BigInt! $token_id: BigInt) { + GetToken( + collectionId: $collection_id + tokenId: { integer: $token_id } + ) { + supply + } +} +``` + +**Variables:** + +```json +{ + "collection_id": 37108, + "token_id": 0 +} +``` + + +

+ Example of converting query args into variables +

+ +### Types + +Types in GraphQL serve as blueprints for defining the shape and composition of the data available through the API, encompassing a collection of fields representing specific pieces of information. + +These structures lay the foundation for shaping queries and mutations, ensuring precise data retrieval and manipulation. + +#### Query: + +A `query` is used to retrieve data from the Enjin Platform. + +In this example, we're querying for information about a specific token. + + + +```graphql +query GetTokenInfo { + GetToken( + collectionId: 7153 + tokenId: { integer: 10 } + ) { + supply # Returns the current supply of the token + } +} +``` + + +```json +{ + "data": { + "TokenInfo": { + "supply": "1000" + } + } +} +``` + + + +The `GetToken` query retrieves token information. + +We specify the `collectionId` and `tokenId` as parameters to identify the token we want. + +The fields `tokenId` and supply indicate the data we want to receive in the response. + +#### Mutation: + +A `mutation` is used to modify data on the Enjin Platform. For example, you can use a mutation to update a user's profile information. + +In this example, we're minting a new token. + + +```graphql +mutation MintToken { + MintToken( + recipient: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" + collectionId: "7154" + params: { + amount: 1 + tokenId: { integer: 6533 } + } + ) { + id # Returns the transaction ID + } +} +``` + + +```json +{ + "data": { + "MintToken": { + "id": "1234" // request ID + } + } +} +``` + + + +The `MintToken` mutation mints a new token. + +We provide the necessary arguments, such as `recipient`, `collectionId`, and `params`, to define the minting process. + +The field `id` indicate the data we want to receive in the response. + +#### Batch Query: + +A batch `query` allows you to send multiple mutation operations in a single request to reduce network overhead. + +Each query have its own set of parameters and return data. + + + +```graphql +query BatchGetTokenInfo { + TokenInfo1: GetToken( + collectionId: 7153 # Specifies the collection ID for the first token + tokenId: { integer: 10 } # Specifies the unique identifier of the first token + ) { + supply # Returns the current supply of the first token + } + TokenInfo2: GetToken( + collectionId: 4553 # Specifies the collection ID for the second token + tokenId: { integer: 55 } # Specifies the unique identifier of the second token + ) { + supply # Returns the current supply of the second token + } +} +``` + + +```json +{ + "data": { + "TokenInfo1": { + "supply": 100 // Current supply of the first token + }, + "TokenInfo2": { + "supply": 200 // Current supply of the second token + } + } +} +``` + + + +In this batch query, we retrieve information for two different tokens with distinct `collectionId` and `tokenId`. + +Each token's data is aliased using `TokenInfo1` and `TokenInfo2`. + +#### Batch Mutation + +A batch `mutation` allows you to send multiple mutation operations in a single request to reduce network overhead. + +Each mutation can have its own set of parameters and return data. + + + +```graphql +mutation { + MintToken1: MintToken( + recipient: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" # Specifies the recipient's address for the first MintToken operation + collectionId: "7154" # Specifies the collection ID for the first MintToken operation + params: { + amount: 1 # Specifies the amount to mint for the first MintToken operation + tokenId: { integer: 6533 } # Specifies the unique identifier of the first token to mint + } + ) { + id # Returns the request ID for the first MintToken operation + } + MintToken2: MintToken( + recipient: "0x1234567890abcdef" # Specifies the recipient's address for the second MintToken operation + collectionId: "7155" # Specifies the collection ID for the second MintToken operation + params: { + amount: 2 # Specifies the amount to mint for the second MintToken operation + tokenId: { integer: 6534 } # Specifies the unique identifier of the second token to mint + } + ) { + id # Returns the request ID for the second MintToken operation + } +} +``` + + +```json +{ + "data": { + "MintToken1": { + "id": "1234" // request ID for the first MintToken operation + }, + "MintToken2": { + "id": "1235" // request ID for the second MintToken operation + } + } +} +``` + + + +In this batch mutation, we have two separate `MintToken` mutations, labeled using the `MintToken1` and `MintToken2` aliases. + +Both mutations are minting two different tokens into two separate addresses. + +### Pagination + +Some GraphQL queries returns results in multiple pages. In GraphQL, these are called "Connections", and each of the elements in the connection is called "Edge". + +For example, the [GetWallet query](https://studio.apollographql.com/public/EnjinPlatform/variant/core/schema/reference/objects/Query?query=GetWallet) returns an object of type [Wallet](https://studio.apollographql.com/public/EnjinPlatform/variant/core/schema/reference/objects/Wallet). Wallet objects contains tokenAccount which is of type [TokenAccountConnection](https://studio.apollographql.com/public/EnjinPlatform/variant/core/schema/reference/objects/TokenAccountConnection). Since this object is of type 'connection', it uses pagination. + +By default, if you don't specify which page to query, the first page will be returned. +To check if the response contains another page for a certain object, use the pageInfo.hasNextPage property from the connection. + + + +```graphql +query FetchingWalletTokens{ + GetWallet(account: "cxKnfok66R8BAuzGcypxqirYcqW7E9Spn4z5UZSVRuUHAcrTQ"){ + tokenAccounts{ + pageInfo{ + hasNextPage #Returns whether there's a next page or not + } + edges{ + node{ + token{ + tokenId + } + balance + } + } + } + } +} +``` + + +```json +{ + "data": { + "GetWallet": { + "tokenAccounts": { + "pageInfo": { + "hasNextPage": true // Indicates that there is another page available. + }, + "edges": [ + { + "node": { + "token": { + "tokenId": "5" + }, + "balance": "1" + } + }, + {...}, + {...} + ] + } + } + } +} +``` + + + +By default, this connection returns 15 edges (tokens) per page. + +:::tip +The amount of edges per page can be adjusted by using slicing. +::: + +To navigate to the next page, use pagination combined with the edge cursor. +First, add the cursor field to the query response. (Alternatively, you can add the pageInfo.endCursor field to fetch the cursor of the last edge in the connection) + + + +```graphql +query FetchingWalletTokens{ + GetWallet(account: "cxKnfok66R8BAuzGcypxqirYcqW7E9Spn4z5UZSVRuUHAcrTQ"){ + tokenAccounts{ + pageInfo{ + hasNextPage + endCursor #Returns The cursor of the last tokenAccount in the current page + } + edges{ + cursor #Returns The tokenAccount cursor + node{ + token{ + tokenId + } + balance + } + } + } + } +} +``` + + +```json +{ + "data": { + "GetWallet": { + "tokenAccounts": { + "pageInfo": { + "hasNextPage": true, + "endCursor": "eyJpZCI6Mjk0NzQsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0" // The cursor of the last tokenAccount in the current page + }, + "edges": [ + { + "cursor": "eyJpZCI6MzU3NCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", // The tokenAccount cursor + "node": { + "token": { + "tokenId": "5" + }, + "balance": "1" + } + }, + {...}, + {...}, + { + "cursor": "eyJpZCI6Mjk0NzQsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", // The tokenAccount cursor + "node": { + "token": { + "tokenId": "11" + }, + "balance": "1" + } + } + ] + } + } + } +} +``` + + + +Now we have the cursor of the last edge of the current page. +To navigate to the next page, add the `after` parameter to the `tokenAccounts` connection, and insert the cursor of the last edge of the current page. + + + +```graphql +query FetchingWalletTokens{ + GetWallet(account: "cxKnfok66R8BAuzGcypxqirYcqW7E9Spn4z5UZSVRuUHAcrTQ"){ + tokenAccounts(after: "eyJpZCI6Mjk0NzQsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0"){ #Indicates that we want to fetch tokenAccounts after the specified cursor. + pageInfo{ + hasNextPage + endCursor + } + edges{ + cursor + node{ + token{ + tokenId + } + balance + } + } + } + } +} +``` + + +```json +{ + "data": { + "GetWallet": { + "tokenAccounts": { + "pageInfo": { + "hasNextPage": true, + "endCursor": "eyJpZCI6NTE2MzgsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0" + }, + "edges": [ + { + "cursor": "eyJpZCI6Mzc2MTYsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "token": { + "tokenId": "9" + }, + "balance": "1" + } + }, + {...}, + {...}, + { + "cursor": "eyJpZCI6NTE2MzgsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "token": { + "tokenId": "1" + }, + "balance": "1" + } + } + ] + } + } + } +} +``` + + + +To get all edges in the connection, continue navigating to the next page with a loop. +You'll know that you reached the last page when the `hasNextPage` returns `false`. + +*** + +With this understanding, you're ready to create any GraphQL requests the Enjin API has to offer. diff --git a/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/05-using-enjin-api.md b/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/05-using-enjin-api.md new file mode 100644 index 0000000..471e156 --- /dev/null +++ b/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/05-using-enjin-api.md @@ -0,0 +1,98 @@ +--- +title: "Using the Enjin API" +slug: "../using-the-enjin-api" +description: "Explore the Enjin API and learn how to easily interact with the Enjin blockchain, mint tokens, and manage digital assets programmatically." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +## Introduction to the Enjin API + +The Enjin API is a set of programmatic interfaces that allow developers to interact with the Enjin Platform from their own applications. Built using , it enables clients to request exactly the data they need in a flexible and efficient way. Unlike traditional RESTful APIs, which require multiple endpoints for different data requirements, GraphQL enables more efficient data retrieval through a single endpoint. This flexibility reduces the number of network requests and optimizes performance, making it particularly advantageous for applications that require efficient data handling, such as blockchain and NFT platforms. + +:::info What you'll need: +- Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) to pay for . +- You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +::: + +:::tip New to GraphQL? +If you're more familiar with REST APIs, it's important to understand that our API uses GraphQL - a flexible and powerful query language for APIs. GraphQL works quite differently from REST, and knowing how to structure your queries and handle responses is essential for successful integration.\ +We recommend reviewing our [How to Use GraphQL guide](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md) to get started quickly and effectively. +::: +## Authentication + +Access tokens assume a pivotal role in facilitating your application's interaction with the Enjin Platform API. +These tokens serve a dual purpose: + +- They grant your application access to the platform. +- They allow for automatic request approval and signing using the Wallet Daemon. + +To make authenticated calls to the Enjin Platform: + +1. login or create an account in the [Testnet Enjin Platform Cloud](https://platform.canary.enjin.io). +2. Create an API token if you haven't already. +3. When making HTTP requests to the GraphQL endpoints, include your API token in the request headers using the `Authorization` field: + +``` +"Authorization": "" +``` + +4. _(Optional)_ To automate requests made from the Enjin Platform using a wallet daemon, you need to configure your daemon with the API token. + For more details head over to [Using the Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) + +## Endpoints & Queries + +Every data transfer strictly adheres to the HTTP/1.1 standard, with HTTPS encryption mandatory for all endpoints to ensure secure communication. + +The Enjin API primarily utilizes GraphQL, meaning you'll be sending POST HTTP requests to specific endpoints with your GraphQL queries and mutations. This allows you to interact with the Enjin blockchain in a flexible and efficient manner. + +:::tip SDKs for Streamlined Development +For developers looking for a more integrated experience, Enjin also provides official **Software Development Kits (SDKs)** for various programming languages. These SDKs wrap the underlying GraphQL API, offering convenient methods and abstractions to streamline development. You can find more information and links to the available SDKs on our [Software Development Kits (SDKs) page](/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md). +::: + +### Overview of GraphQL Endpoints + +The Enjin API is structured around four distinct GraphQL endpoints, each designed to handle specific sets of queries and mutations. This segmentation allows for efficient and organized interactions tailored to different functional areas within the Enjin ecosystem: + +1. **Core Operations:** This endpoint handles fundamental blockchain operations such as creating collections, minting and transferring s, freezing, and burning s . It provides the essential tools for managing and interacting with s on the Enjin Blockchain. + +2. **Marketplace:** Focused on marketplace activities, this endpoint encompasses operations like listing tokens for sale, purchasing tokens, and bidding on auctions. It facilitates seamless transactions and interactions within the Enjin Marketplace. + +3. **Beam:** The Beam endpoint manages operations related to the Beam system, which is used for distributing tokens claimable via QR codes. This feature is particularly useful for promotional activities and engaging user experiences. + +4. **Fuel Tanks:** This endpoint is dedicated to the Fuel Tank system, which allows for subsidizing transaction fees. It supports operations that manage and configure fuel tanks, enabling cost-effective transactions on the blockchain. + +GraphQL queries and mutations are executed by sending POST HTTP requests to the corresponding endpoint: + +:::note GraphQL Endpoints +### Testnet: +- **Core Operations** `https://platform.canary.enjin.io/graphql` +- **Marketplace** `https://platform.canary.enjin.io/graphql/marketplace` +- **Beam** `https://platform.canary.enjin.io/graphql/beam` +- **Fuel Tanks** `https://platform.canary.enjin.io/graphql/fuel-tanks` +### Mainnet: +- **Core Operations** `https://platform.enjin.io/graphql` +- **Marketplace** `https://platform.enjin.io/graphql/marketplace` +- **Beam** `https://platform.enjin.io/graphql/beam` +- **Fuel Tanks** `https://platform.enjin.io/graphql/fuel-tanks` +::: +Here is an example of an HTTP cURL post request to create a collection on the Canary blockchain, with the API Token provided: + +``` +curl --location --request POST 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Insert your API token here' \ +-d '{"query":"mutation CreateCollection($forceCollapsingSupply: Boolean) {\r\n CreateCollection(\r\n mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"forceCollapsingSupply":false}}' +``` + +## API Reference + +For a comprehensive guide on exploring and utilizing GraphQL queries and mutations in the Enjin API, please refer to our [API Reference](/03-api-reference/03-api-reference.md). + +*** + +:::tip What's next? +- New to GraphQL? Learn how to structure queries and mutations in our [How to Use GraphQL guide](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md). +- To automate signing requests, continue to the [Using the Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) page. +- Or, If you're ready to start building... [Create a Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md). diff --git a/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/_category_.json b/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/_category_.json new file mode 100644 index 0000000..e44d142 --- /dev/null +++ b/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Using the Enjin API" +} diff --git a/versioned_docs/version-2.x/01-getting-started/06-using-wallet-daemon.md b/versioned_docs/version-2.x/01-getting-started/06-using-wallet-daemon.md new file mode 100644 index 0000000..49d1102 --- /dev/null +++ b/versioned_docs/version-2.x/01-getting-started/06-using-wallet-daemon.md @@ -0,0 +1,172 @@ +--- +title: "Using the Wallet Daemon" +slug: "using-wallet-daemon" +description: "Get started with the Enjin Wallet Daemon, an automated tool for managing blockchain transactions and assets securely and efficiently." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +The Enjin Wallet Daemon provides a streamlined process for signing blockchain transactions, enabling seamless and efficient transactions between your game and the blockchain. This tool creates a persistent bridge between your game and the blockchain, ensuring a fluid gaming experience for players. + +In the Enjin Platform context, the Wallet Daemon is a utility tool that manages a blockchain wallet address associated with an Enjin Blockchain account. When a transaction is initiated on the Enjin Platform, the Wallet Daemon receives the transaction, signs it, and sends it back to the platform. This ensures secure and efficient transaction processing for Enjin Platform users. + +![A diagram of the Enjin Platform ](/img/getting-started/enjin-platform-diagram.png) + +The diagram above provides insight into the interaction between the Enjin Wallet Daemon and the Enjin Platform. This illustrates how the Wallet Daemon can communicate with the API in both directions, automatically signing and broadcasting transactions to the blockchain. This helps developers better understand how the two components work together seamlessly to provide a streamlined experience. + +## Wallet Daemon Events + +![A diagram of the wallet daemon](/img/getting-started/wallet-daemon-diagram.png) + +The diagram above depicts the various stages involved in creating a new collection on the Enjin Matrixchain via the Enjin Platform API, with a particular emphasis on the Wallet Daemon events. + +1. **Establish a WebSocket connection -** Connect to a WebSocket server to subscribe to channels that receive events. +2. **Subscribe to a WebSocket channel -** Subscribe to the channel associated with your wallet to receive real-time events. +3. **Send a mutation -** Send a "CreateCollection" mutation to the Enjin Platform API to create a new collection on the Enjin Matrixchain. The API will encode the transaction that the wallet daemon needs to sign and save it in the Enjin Platform Database. +4. **Wallet daemon requests pending transactions -** The wallet daemon repeatedly asks the Enjin Platform API if there are any transactions to sign to prevent incoming connections to the wallet daemon, which holds your . +5. **Sign and broadcast the transaction -** After receiving the pending transaction, the wallet daemon signs it with your private key and broadcasts it to the Enjin Matrixchain. +6. **Enjin Matrixchain processes the transaction -** If everything is correct and valid, the Matrixchain successfully processes the extrinsic sent by the wallet daemon, and the new collection is created. +7. **Enjin Platform API monitors the chain -** The Enjin Platform API continually watches the chain via a WebSocket connection to detect any activity. +8. **Save the data to the database -** The Enjin Platform API stores the new collection data in its own database, allowing you to query it as desired. +9. **Emit an event -** The Enjin Platform API sends an event to the WebSocket channels, providing you with the opportunity to take appropriate actions if you're subscribed to the relevant channel. + +*** + +:::info There are two approaches for running the Wallet Daemon: +- Using the [Wallet Daemon Executable](#wallet-daemon-executable). +- Setting up [Wallet Daemon via Docker](#setting-up-wallet-daemon-via-docker). +::: + +:::tip Choosing the Right Approach for Running the Wallet Daemon +There are two ways to run the Wallet Daemon, each suitable for different user profiles and use cases: +1. **Executable Approach:** This method involves running a simple executable, configuring it via a user interface, and clicking 'Run' to start it. It is the most straightforward approach and is recommended for beginners or those who are not developers and for development environments / personal use. Learn how to set it up in our [Setting up Wallet Daemon using Executable](#wallet-daemon-executable) guide. +2. **Docker Approach:** This method involves cloning a GitHub repository and modifying the configuration files. It offers more flexibility and control, making it suitable for developers and for production use. Learn how to set it up in our [Setting up Wallet Daemon using Docker](#setting-up-wallet-daemon-via-docker) guide. + +Choose the approach that best fits your technical proficiency and the needs of your project. +::: + +## Wallet Daemon Executable + +![The wallet daemon executable app](/img/getting-started/wallet-daemon-executable-welcome.png) + +Setting up the Wallet Daemon Executable is a straight forward process. +Download the latest version of the Wallet Daemon from [GitHub](https://github.com/enjin/wallet-daemon-ui/releases), extract it, and run the `enjin_wallet_daemon.exe` executable file. +Follow the on-screen instructions to set it up and make sure to insert your Enjin Platform API Token from your [account settings page](https://platform.canary.enjin.io/settings). + +![Configuring the Wallet Daemon Executable with Enjin Platform](/img/getting-started/daemon-exec-overview.gif) + +## Setting up Wallet Daemon via Docker + +:::note Code repository +The code repository can be found at https://github.com/enjin/wallet-daemon/ +::: + +It is recommended that the Enjin Wallet Daemon is installed and ran in isolation. This means running it on a dedicated server. The daemon itself is incredibly light-weight and does not require any extensive resources. + +:::tip Have Docker installed? +If you don't have Docker installed, you may install either the docker engine or docker desktop, though if you have never used docker before we recommend using docker desktop: https://www.docker.com/products/docker-desktop +::: + +:::warning Action Required: [Sentosa Upgrade](https://enjin.io/blog/enjin-blockchain-sentosa-upgrade) +To ensure uninterrupted service and transaction signing capabilities, users must upgrade their Wallet Daemon to v2.1.1 or greater before the [Sentosa blockchain upgrade](https://enjin.io/blog/enjin-blockchain-sentosa-upgrade). + +> Canary Testnet: The Sentosa upgrade is already live (since October 27, 2025). If you are using the Canary Wallet Daemon to sign transactions, you must update immediately to v2.1.1+ or transactions will fail. + +> Mainnet: The upgrade is scheduled for December 8, 2025 (approx. 16:30 UTC, at Relaychain block 13229687 and Matrixchain block 7734855). While older versions will work until this date, it is highly recommended to upgrade to the latest version beforehand. + +Instructions on upgrading the wallet-daemon to the latest version can be found [here](#updating-the-daemon-to-the-latest-version) +::: + +To download the Enjin Daemon, clone the `enjin/wallet-daemon` repository from GitHub by running this command in the Terminal: + +```bash +git clone https://github.com/enjin/wallet-daemon.git +``` + +### Configure Daemon + +You will want to update the `.env` file with the `PLATFORM_KEY` (which is an API Token generated within the Settings page of the Enjin Platform Cloud). In addition to this, you'll want to specify a unique `KEY_PASS`. + +:::danger Important note +The `KEY_PASS` is immutable. It is directly used to derive the wallet private key. Choose something unique and make sure to backup this `KEY_PASS` in a secure manner. +::: + +The final configuration is to update the `config.json` file to communicate with either the Enjin Blockchain (mainnet) or Canary Blockchain (testnet). You can do this by updating the node property in the JSON file. You can find an example of both networks below. + + + +``` +{ + "node": "wss://rpc.matrix.blockchain.enjin.io:443", + "relay_node": "wss://rpc.relay.blockchain.enjin.io:443", + "api": "https://platform.enjin.io/graphql", + "master_key": "store" +} +``` + + +``` +{ + "node": "wss://rpc.matrix.canary.enjin.io:443", + "relay_node": "wss://rpc.relay.canary.enjin.io:443", + "api": "https://platform.canary.enjin.io/graphql", + "master_key": "store" +} +``` + + + +### Starting the Daemon + +Finally, you can spin up the daemon using the following command from your local `wallet-daemon` directory: + +`docker compose up -d daemon` + +On the initial launch, a 12-words mnemonic seed will be created in the `/store` folder, with an additional encryption using the secret pass provided in the `KEY_PASS` environment variable. If you ever need to import this wallet into any wallet app, the derivation path is as follows: +`///` + +### Updating the Daemon to the latest version + +To upgrade your Docker installation of the Wallet Daemon to the latest version, follow these steps from within your local `wallet-daemon` directory: + +1. **Pull the latest code from the repository:** + ```bash + git pull + ``` +2. **Stop the currently running container:** + ```bash + docker compose stop daemon + ``` +3. **Rebuild the Docker image to incorporate the updates:** + ```bash + docker compose build --no-cache + ``` +4. **Start the daemon again:** + ```bash + docker compose up -d daemon + ``` + +### Importing Wallet Daemon From Existing Seed + +:::info Wallet Daemon Encryption +Wallet Daemon may be encrypted with a password specified in the `KEY_PASS` env var located in the `.env` file. +If your existing wallet is encrypted with a password, make sure to update your `KEY_PASS` var accordingly. +If your existing wallet is not encrypted, leave the `KEY_PASS` var empty. +::: + +Follow the steps below to set up your wallet daemon from an existing seed: + +1. Find your wallet's public key. You can convert your wallet's account address to it's public key with the [Account Format Transform](https://matrix.subscan.io/tools/format_transform) tool. + We'll use wallet address `efRP7f5aFWWobNiNxcWGNxhY1RdRXZ4kScvwuFdD4bsBHEUZW` as an example. It's public key is `0x62c75d8f81e05794cd0b703cf07b7ea3196840eaac4e300cb968fdd266882e02`. +2. Remove the 0x from the public key from step #1. + For our example, it's `62c75d8f81e05794cd0b703cf07b7ea3196840eaac4e300cb968fdd266882e02` +3. Inside your local `wallet-daemon/store` folder, create a file with the name `73723235`. + For our example, we've created the file `/store/7372323562c75d8f81e05794cd0b703cf07b7ea3196840eaac4e300cb968fdd266882e02` +4. Inside the new file created on step #3, insert your wallet's mnemonic seed wrapped with double quotes. + Example: `"earn meat maid rotate ..."` +5. Rebuild your docker container for the changes to take effect: + `docker compose build` +6. Run the daemon again: + `docker compose up daemon` diff --git a/versioned_docs/version-2.x/01-getting-started/_category_.json b/versioned_docs/version-2.x/01-getting-started/_category_.json new file mode 100644 index 0000000..1ff6c67 --- /dev/null +++ b/versioned_docs/version-2.x/01-getting-started/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Getting Started", + "link": { + "type": "generated-index", + "description": "Begin your journey with the Enjin ecosystem here. This section provides essential introductory guides and foundational knowledge." + } +} + \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/01-creating-collections.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/01-creating-collections.md new file mode 100644 index 0000000..b981831 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/01-creating-collections.md @@ -0,0 +1,271 @@ +--- +title: "Creating Collections" +slug: "creating-collections" +description: "Discover how to create and manage collections in the Enjin ecosystem, allowing you to organize blockchain assets efficiently." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info What you'll need: +- Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for and 6.25 ENJ for s. +You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +::: + +On Enjin Blockchain, every token must be placed inside a . + +**There are two ways to use the functionalities:** + +1. [Using the Enjin Dashboard](#option-a-using-the-enjin-dashboard) +2. [Using the Enjin API & SDKs](#option-b-using-the-enjin-api--sdks) + +## Option A. Using the Enjin Dashboard + +In the Platform menu, navigate to "**[Collections](https://platform.canary.enjin.io/collections)**". Then, click the "**[Create Collection](https://platform.canary.enjin.io/create/collection)**" button. + +![Creating a collection](/img/guides/managing-tokens/create-collection.gif) + +From here, you can customize your collection's Mint Policy, Market Policy, Explicit Currencies (on option to delegate a token for ), and Attributes. + +- **[Mint Policy](/03-api-reference/04-important-arguments.md#mintpolicy) -** The rules pertaining to token supply and number of tokens available to be minted in the future. +- **[Market Policy](/03-api-reference/04-important-arguments.md#marketpolicy) -** Determines the rules which tokens in this collection must follow when interacting with the on-chain marketplace. +- **[Explicit Royalty Currencies](/03-api-reference/04-important-arguments.md#explicitroyaltycurrencies) -** Choose which currencies are required to pay marketplace royalties for the tokens in this collection. +- **[Attributes](/03-api-reference/04-important-arguments.md#attributes) -** Add details to your collection using key-value pairs. Standard keys like `name` and `description` allow applications to display your content correctly. You can also use the `URI` to link to a JSON file hosting your metadata. For more information, see the [Metadata Standard](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md) page. + +:::info Learn more about the arguments +For a comprehensive view and detail of all available arguments please refer to our [API Reference](/03-api-reference/03-api-reference.md). +::: + +Once you're satisfied with the options, click on the "**Create**" button at the bottom right corner to create the request. + +The Transaction Request will then appear in the "**Transactions**" menu. + +

+ Collection created banner that appears after creating a transaction request +

+ +![Pending create collection transaction](/img/guides/managing-tokens/pending-create-collection-txn.png) + +Since this request requires a , it'll need to be signed with your Wallet. + +- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. +- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. + +Once you've created a collection you're ready to start [creating tokens](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md). + +## Option B. Using the Enjin API & SDKs + +The `CreateCollection` mutation is used to create a new on-chain collection. + + + +```graphql +mutation CreateCollection { + CreateCollection(mintPolicy: { forceCollapsingSupply: false }) #Set to true to enforce collapsing supply mint policy + { + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation CreateCollection($forceCollapsingSupply: Boolean) {\r\n CreateCollection(\r\n mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"forceCollapsingSupply":false}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Setup the mutation +var createCollection = new CreateCollection() + .SetMintPolicy(new MintPolicy().SetForceCollapsingSupply(false)); //Set to true to enforce collapsing supply mint policy + +// Define and assign the return data fragment to the mutation +var transactionFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +createCollection.Fragment(transactionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendCreateCollection(createCollection); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreMutations.hpp" +#include +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Setup mutation data + shared_ptr mintPolicy = make_shared(); + mintPolicy->SetForceSingleMint(make_shared(false)); + + // Setup mutation + CreateCollection createCollection = CreateCollection(); + createCollection.SetMintPolicy(mintPolicy); + + // Define and assign the return data fragment to the mutation + shared_ptr transactionFragment = make_shared(); + transactionFragment + ->WithId() + .WithMethod() + .WithState(); + + createCollection.SetFragment(transactionFragment); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>>> futureResponse = SendCreateCollection(*client, createCollection); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& transaction = gqlResult->GetData()->GetResult(); + + std::cout << to_string(transaction->GetId().value()) << std::endl; + std::cout << ToString(transaction->GetMethod().value()) << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation CreateCollection($forceCollapsingSupply: Boolean) { + CreateCollection( + mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } + ) { + id + method + state + } + } + `, + variables: { + forceCollapsingSupply: false //Set to true to enforce collapsing supply mint policy + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation CreateCollection($forceCollapsingSupply: Boolean) { + CreateCollection( + mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } + ) { + id + method + state + } + } + `, + variables: { + forceCollapsingSupply: false //Set to true to enforce collapsing supply mint policy + } +}, { + headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation CreateCollection($forceCollapsingSupply: Boolean) { + CreateCollection( + mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } + ) { + id + method + state + } +} +''' + +variables = {'forceCollapsingSupply': False} #Set to true to enforce collapsing supply mint policy + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +:::info Explore More Arguments +For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. +For instance, you'll find settings such as different supply types with the `MintPolicy` argument, enforcing royalties with the `MarketPolicy` argument, or adding metadata with the `AttributeInput` argument. +::: + +A WebSocket event will also be fired so you can pick up the collection in real time by listening to the app channel on the WebSocket. + +:::tip What's next? +You've created a collection, now [Fill it with Tokens](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) +::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md new file mode 100644 index 0000000..71e9273 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md @@ -0,0 +1,343 @@ +--- +title: "Transfer / Accept collection ownership" +slug: "transfer-accept-collection" +description: "Explore how to transfer and accept entire blockchain collections, allowing for seamless movement of grouped assets within the Enjin ecosystem." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info What you'll need: +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +- If you are transferring collection ownership, you also need to own a [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md). +::: + +Collection ownership on the Enjin Blockchain grants specific permissions and control over a collection and its tokens. +The collection owner is the only account authorized to make changes to the collection or any tokens within it. This includes the exclusive ability to mint new tokens or mint additional supply of existing tokens in the collection. + +Transferring collection ownership is done in two steps: + +## Step #1: Sending a transfer ownership request with the [Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) + +To send a transfer ownership request, we use the `MutateCollection` mutation: + + + +```graphql +mutation SendTransferOwnershipRequest { + MutateCollection( + collectionId: 36105 #Specify the collection ID + mutation: { + owner: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the new owner + } + ) { + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation SendTransferOwnershipRequest(\r\n $collection_id: BigInt!\r\n $new_owner: String\r\n) {\r\n MutateCollection(\r\n collectionId: $collection_id\r\n mutation: { owner: $new_owner }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"new_owner":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Set up the collection mutation input. +var collectionMutationInput = new CollectionMutationInput() + .SetOwner("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"); // Set the new owner account of the collection. + +// Set up the mutation +var mutateCollection = new MutateCollection() + .SetCollectionId(36105) // Specify the collection ID. + .SetMutation(collectionMutationInput); // Specify the mutation input. + +// Define and assign the return data fragment to the mutation +var transactionFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +mutateCollection.Fragment(transactionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendMutateCollection(mutateCollection); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +Work in progress +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation SendTransferOwnershipRequest( + $collection_id: BigInt! + $new_owner: String + ) { + MutateCollection( + collectionId: $collection_id + mutation: { + owner: $new_owner + } + ) { + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + new_owner: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the new owner + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation SendTransferOwnershipRequest( + $collection_id: BigInt! + $new_owner: String + ) { + MutateCollection( + collectionId: $collection_id + mutation: { + owner: $new_owner + } + ) { + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + new_owner: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the new owner + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation SendTransferOwnershipRequest( + $collection_id: BigInt! + $new_owner: String +) { + MutateCollection( + collectionId: $collection_id + mutation: { + owner: $new_owner + } + ) { + id + method + state + } +} +''' + +variables = { + 'collection_id': 36105, #Specify the collection ID + 'new_owner': 'cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f' #Specify the new owner + +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +Once the transaction is confirmed, the new owner needs to approve the ownership transfer request, for the collection ownership to be executed + +## Step #2: Accepting a transfer ownership request with the [Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) + +To accept a transfer ownership request, we call the `AcceptCollectionTransfer` mutation from the new collection owner account: + + + +```graphql +mutation AcceptTransferOwnershipRequest { + AcceptCollectionTransfer( + collectionId: 36105 #Specify the collection ID + ) { + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation AcceptTransferOwnershipRequest($collection_id: BigInt!) {\r\n AcceptCollectionTransfer(collectionId: $collection_id) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Set up the mutation +var acceptCollectionTransfer = new AcceptCollectionTransfer() + .SetCollectionId(36105); // Specify the collection ID. + +// Define and assign the return data fragment to the mutation +var transactionFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +acceptCollectionTransfer.Fragment(transactionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendAcceptCollectionTransfer(acceptCollectionTransfer); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +Work in progress +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation AcceptTransferOwnershipRequest( + $collection_id: BigInt! + ) { + AcceptCollectionTransfer( + collectionId: $collection_id + ) { + id + method + state + } + } + `, + variables: { + collection_id: 36105 //Specify the collection ID + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation AcceptTransferOwnershipRequest( + $collection_id: BigInt! + ) { + AcceptCollectionTransfer( + collectionId: $collection_id + ) { + id + method + state + } + } + `, + variables: { + collection_id: 36105 //Specify the collection ID + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation AcceptTransferOwnershipRequest( + $collection_id: BigInt! +) { + AcceptCollectionTransfer( + collectionId: $collection_id + ) { + id + method + state + } +} +''' + +variables = { + 'collection_id': 36105 #Specify the collection ID + +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +Once the transaction is confirmed, the collection ownership will be transferred to the new owner. diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md new file mode 100644 index 0000000..c0c1bac --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md @@ -0,0 +1,286 @@ +--- +title: "Creating a Currency Token" +slug: "../creating-a-currency-token" +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Similarly to [Tokens](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md), "Currency Tokens" are also digital assets that can be traded, sold, or used on the Enjin Blockchain. However, unlike standard tokens, currency tokens support fractional values, allowing them to be divided into smaller units, just like traditional currencies. + +:::info What you'll need: +- Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to process transactions and at least 0.01 ENJ for the . +You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) to place the tokens in. +::: + +Enjin Blockchain allows you to create customized structures. This flexibility enables you to organize your tokens in various ways that suit your needs. + +For the currency token options, configure the metadata parameter, which consists of 3 values: + +- **Name:** The token name (e.g. Gold Coins). +- **Symbol:** A short symbol that represents the token (e.g. GOLD). +- **Decimals:** Max amount of decimals supported for this token (e.g. 2 would mean the token can support up to 2 decimals, like 5.75 Gold Coins). + +:::warning Important Note on Decimals Support +The `decimalCount` property specifies how token balances should be displayed in applications, but it does not allow for actual fractional values on the Enjin Blockchain. +This means that for a token with "Decimals: 2", the balance should be divided by 100 (10^2) when displayed in applications. +For example, for the Gold Coins token example mentioned above, a balance of 1,575 Gold Coins should be shown as 15.75 (1,575/100) in apps. +Similarly, when minting tokens, to mint 4.80 Gold Coins, the minted supply parameter should be set to 480 (4.8 \* 10^2). +::: + +:::tip Token ID Structure [Best Practices](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md) +Before minting the Mainnet versions of your Tokens, that will be used in your live economy. Make sure to take a look at the [best practices for Token ID structure](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md). +::: + +**There are two ways to use the functionalities:** + +- [Using the Platform User Interface](#option-a-using-the-enjin-dashboard) +- [Using the GraphQL API](#option-b-using-the-enjin-api--sdks) + +## Option A. Using the Enjin Dashboard + +:::warning Unavailable via the User Interface +The option to create a currency token via the user interface is currently being developed. +For the time being, you can create currency token via [Option B. Using the Enjin API & SDKs](#option-b-using-the-enjin-api--sdks). +::: + +## Option B. Using the Enjin API & SDKs + +CreateToken mutation enables you to create a new token within an existing collection. This operation is essential for introducing new digital assets, and it allows you to define various attributes and characteristics for the newly created token. +For the currency token options, configure the metadata parameter, which consists of name, symbol and decimals + + + +```graphql +mutation CreateCurrencyToken{ + CreateToken( + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #The recipient of the initial supply + collectionId: 2406 #Set the collection ID + params:{ + tokenId: {integer: 0} #Set the token ID + initialSupply: 1 #Mint initial supply + cap: {type: INFINITE} #Define supply type + metadata: {name: "Gold Coins", symbol: "GOLD", decimalCount: 2} #Define currency configuration here + } + ) { + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation CreateCurrencyToken(\r\n $recipient: String!\r\n $collection_id: BigInt!\r\n $token_id: BigInt\r\n $initial_supply: BigInt\r\n $cap: TokenMintCapType!\r\n $metadata: TokenMetadataInput\r\n) {\r\n CreateToken(\r\n recipient: $recipient\r\n collectionId: $collection_id\r\n params: {\r\n tokenId: { integer: $token_id }\r\n initialSupply: $initial_supply\r\n cap: { type: $cap }\r\n metadata: $metadata\r\n }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","collection_id":2406,"token_id":0,"initial_supply":1,"cap":"INFINITE","metadata":{"name":"Gold Coins","symbol":"GOLD","decimalCount":2}}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Define the token metadata input +var tokenMetadata = new TokenMetadataInput() + .SetName("Token Name") // Set the token name + .SetSymbol("TKN") // Set the token symbol + .SetDecimalCount(18); // Set the decimal count + +// Define the token parameters +var tokenParams = new CreateTokenParams() + .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) //Set the token ID + .SetInitialSupply(1) //Mint initial supply + .SetCap(new TokenMintCap().SetType(TokenMintCapType.Infinite)) //Define supply type + .SetMetadata(tokenMetadata); //Set the token metadata + +// Set up the mutation +var createToken = new CreateToken() + .SetRecipient("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f") //The recipient of the initial supply + .SetCollectionId(2406) //Set the collection ID + .SetParams(tokenParams); //Set the previously defined token params + +// Define and assign the return data fragment to the mutation +var createTokenFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +createToken.Fragment(createTokenFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendCreateToken(createToken); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +Work in Progress! +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation CreateCurrencyToken + ( + $recipient: String! + $collection_id: BigInt! + $token_id: BigInt + $initial_supply: BigInt + $cap: TokenMintCapType! + $metadata: TokenMetadataInput + ) { + CreateToken( + recipient: $recipient + collectionId: $collection_id + params:{ + tokenId: {integer: $token_id} + initialSupply: $initial_supply + cap: {type: $cap} + metadata: $metadata + } + ) { + id + method + state + } + } + `, + variables: { + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //The recipient of the initial supply + collection_id: 2406, //Specify the collection ID + token_id: 0, //Specify the token ID + initial_supply: 1, //Mint initial supply + cap: "INFINITE", //Define supply type + metadata: {name: "Gold Coins", symbol: "GOLD", decimalCount: 2} //Define currency configuration here + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation CreateCurrencyToken( + $recipient: String! + $collection_id: BigInt! + $token_id: BigInt + $initial_supply: BigInt + $cap: TokenMintCapType! + $metadata: TokenMetadataInput + ) { + CreateToken( + recipient: $recipient + collectionId: $collection_id + params: { + tokenId: { integer: $token_id } + initialSupply: $initial_supply + cap: { type: $cap } + metadata: $metadata + } + ) { + id + method + state + } + } + `, + variables: { + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //The recipient of the initial supply + collection_id: 2406, //Specify the collection ID + token_id: 0, //Specify the token ID + initial_supply: 1, //Mint initial supply + cap: "INFINITE", //Define supply type + metadata: {name: "Gold Coins", symbol: "GOLD", decimalCount: 2} //Define currency configuration here + } +}, { + headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation CreateCurrencyToken( + $recipient: String! + $collection_id: BigInt! + $token_id: BigInt + $initial_supply: BigInt + $cap: TokenMintCapType! + $metadata: TokenMetadataInput +) { + CreateToken( + recipient: $recipient + collectionId: $collection_id + params: { + tokenId: { integer: $token_id } + initialSupply: $initial_supply + cap: { type: $cap } + metadata: $metadata + } + ) { + id + method + state + } +} +''' + +variables = { + 'recipient': 'cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f', #The recipient of the initial supply + 'collection_id': 2406, #Specify the collection ID + 'token_id': 0, #Specify the token ID + 'initial_supply': 1, #Mint initial supply + 'cap': 'INFINITE', #Define supply type + 'metadata': {'name': "Gold Coins", 'symbol': "GOLD", 'decimalCount': 2} #Define currency configuration here +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +A WebSocket event will also be fired so you can pick up the changes in real-time by listening to the app channel on the WebSocket. + +:::tip +For Token ID management, head to [Best Practices > TokenID Structure](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md) +::: + +:::info Explore More Arguments +For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. +For instance, you'll find settings such as adding attributes/royalties/supply type and much more with the `CreateTokenParams` argument, or the ability to sign using a managed wallet with the `signingAccount` argument. +::: + +:::tip What's next? +To add metadata to your token, go to the [Adding Metadata](/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md) tutorial. +::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md new file mode 100644 index 0000000..82edea4 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md @@ -0,0 +1,364 @@ +--- +title: "Creating Tokens" +slug: "../creating-tokens" +description: "A step-by-step guide on how to create custom tokens using the Enjin Platform, enabling developers to mint blockchain assets with ease." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +"s" are digital assets that can be traded, sold, or used on the Enjin Blockchain. + +:::info What you'll need: +- Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to process transactions and at least 0.01 ENJ for the . +You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) to place the tokens in. +::: + +Enjin Blockchain allows you to create customized structures. This flexibility enables you to organize your tokens in various ways that suit your needs. + +- **For s** These tokens are identical in value and function, and therefore, they all share the same Token ID. This commonality in ID reflects their interchangeable nature. For instance, all units of a specific cryptocurrency like Bitcoin would have the same identification as they hold the same value and are indistinguishable from one another in terms of usage and worth. +- **For (s):** Every NFT has a unique Token ID that sets it apart from other tokens, even within the same collection. This unique ID is crucial for establishing the individuality and provenance of each NFT, which could represent anything from digital art to ownership rights over a virtual asset. + +:::tip Token ID Structure [Best Practices](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md) +Before minting the Mainnet versions of your Tokens, that will be used in your live economy. Make sure to take a look at the [best practices for Token ID structure](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md). +::: + +**There are two ways to use the functionalities:** + +- [Using the Platform User Interface](#option-a-using-the-enjin-dashboard) +- [Using the GraphQL API](#option-b-using-the-enjin-api--sdks) + +## Option A. Using the Enjin Dashboard + +In the Platform menu, navigate to "**[Tokens](https://platform.enjin.io/tokens)**". Then, click the "**[Create Token](https://platform.enjin.io/create/token)**" button. + +![Creating Token](/img/guides/managing-tokens/creating-token.gif) + +From here, you can customize your collection's Mint Policy, Market Policy, and Attributes. + +- **Create Token Section -** Basic token options. Make sure to select the Collection ID you wish to mint the token in, the token ID, and the recipient in the corresponding fields. + Make sure to check out the [TokenID Structure Best Practices](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md). +- **[Cap](/03-api-reference/04-important-arguments.md#cap) -** The token cap (if required). + - The **[Infinite](/03-api-reference/04-important-arguments.md#infinite)** supply type is the most flexible. With this model, there is no limit to how many tokens can be minted or be in circulation. The collection owner can always mint additional units, making it ideal for use cases that require an ever-expanding token supply. + - The **[Fixed](/03-api-reference/04-important-arguments.md#supply)** supply type offers a balanced approach between flexibility and control. This model allows the collection owner to mint new tokens as long as the circulating supply does not exceed the predetermined max supply. Burned tokens can be re-minted, ensuring the total supply remains constant. + - The **[Collapsing](/03-api-reference/04-important-arguments.md#collapsing_supply)** supply type is the most strict. This supply type allows the collection owner to mint new tokens as long as the circulating supply does not exceed the max supply. However, burning tokens reduces the max supply, meaning burned tokens cannot be re-minted. This ensures a non-increasing supply, suitable for use cases that require strict control over the token's total amount in circulation. +- **[Token Royalty Settings](/03-api-reference/04-important-arguments.md#hasroyalty) -** Configures royalties for each marketplace sale of this token. +- **[Attributes](/03-api-reference/04-important-arguments.md#attributes) -** Add details to your token using key-value pairs. Standard keys like `name` and `description` allow applications to display your content correctly. You can also use the `URI` to link to a JSON file hosting your metadata. For more information, see the [Metadata Standard](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md) page. + +:::info Learn more about the arguments +For a comprehensive view and detail of all available arguments please refer to our [API Reference](/03-api-reference/03-api-reference.md). +::: + +Once you're satisfied with the options, click on the "**Create**" button at the bottom right corner to create the request. + +The Transaction Request will then appear in the "**Transactions**" menu. + +

+ Create Token Transaction Request Banner +

+ +![Pending Create Token Transaction](/img/guides/managing-tokens/pending-create-token-txn.png) + +Since this request requires a , it'll need to be signed with your Wallet. + +- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. +- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. + +Once your token is created, lets give it a new look by [Adding Metadata](/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md) + +## Option B. Using the Enjin API & SDKs + +CreateToken mutation enables you to create a new token within an existing collection. This operation is essential for introducing new digital assets, and it allows you to define various attributes and characteristics for the newly created token. + + + +```graphql +mutation CreateToken{ + CreateToken( + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #The recipient of the initial supply + collectionId: 2406 #Set the collection ID + params:{ + tokenId: {integer: 0} #Set the token ID + initialSupply: 1 #Mint initial supply + cap: {type: INFINITE}} #Define supply type + ) { + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation CreateToken(\r\n $recipient: String!\r\n $collection_id: BigInt!\r\n $token_id: BigInt\r\n $initial_supply: BigInt\r\n $cap: TokenMintCapType!\r\n) {\r\n CreateToken(\r\n recipient: $recipient\r\n collectionId: $collection_id\r\n params: {\r\n tokenId: { integer: $token_id }\r\n initialSupply: $initial_supply\r\n cap: { type: $cap }\r\n }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","collection_id":91829,"token_id":0,"initial_supply":1,"cap":"INFINITE"}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Define the token parameters +var tokenParams = new CreateTokenParams() + .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) //Set the token ID + .SetInitialSupply(1) //Mint initial supply + .SetCap(new TokenMintCap().SetType(TokenMintCapType.Infinite)); //Define supply type + +// Setup the mutation +var createToken = new CreateToken() + .SetRecipient("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f") //The recipient of the initial supply + .SetCollectionId(2406) //Set the collection ID + .SetParams(tokenParams); //Set the previously defined token params + +// Define and assign the return data fragment to the mutation +var createTokenFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +createToken.Fragment(createTokenFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendCreateToken(createToken); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreMutations.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Setup mutation data + shared_ptr tokenParams = make_shared(); + + shared_ptr tokenId = make_shared(); + tokenId->SetInteger(make_shared("0")); + + shared_ptr tokenMintCap = make_shared(); + tokenMintCap->SetType(TokenMintCapType::Infinite); + + tokenParams->SetTokenId(tokenId); + tokenParams->SetInitialSupply(make_shared("1")); + tokenParams->SetCap(tokenMintCap); + + // Setup mutation + CreateToken createToken = CreateToken(); + createToken + .SetRecipient(make_shared("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f")) + .SetCollectionId(make_shared("2406")) + .SetParams(tokenParams); + + // Define and assign the return data fragment to the mutation + shared_ptr transactionFragment = make_shared(); + transactionFragment + ->WithId() + .WithMethod() + .WithState(); + + createToken.SetFragment(transactionFragment); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>>> futureResponse = SendCreateToken(*client, createToken); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& transaction = gqlResult->GetData()->GetResult(); + + std::cout << to_string(transaction->GetId().value()) << std::endl; + std::cout << ToString(transaction->GetMethod().value()) << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation CreateToken( + $recipient: String! + $collection_id: BigInt! + $token_id: BigInt + $initial_supply: BigInt + $cap: TokenMintCapType! + ) { + CreateToken( + recipient: $recipient + collectionId: $collection_id + params: { + tokenId: { integer: $token_id } + initialSupply: $initial_supply + cap: { type: $cap } + } + ) { + id + method + state + } + } + `, + variables: { + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //The recipient of the initial supply + collection_id: 2406, //Specify the collection ID + token_id: 0, //Specify the token ID + initial_supply: 1, //Mint initial supply + cap: "INFINITE" //Define supply type + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation CreateToken( + $recipient: String! + $collection_id: BigInt! + $token_id: BigInt + $initial_supply: BigInt + $cap: TokenMintCapType! + ) { + CreateToken( + recipient: $recipient + collectionId: $collection_id + params: { + tokenId: { integer: $token_id } + initialSupply: $initial_supply + cap: { type: $cap } + } + ) { + id + method + state + } + } + `, + variables: { + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //The recipient of the initial supply + collection_id: 2406, //Specify the collection ID + token_id: 0, //Specify the token ID + initial_supply: 1, //Mint initial supply + cap: "INFINITE" //Define supply type + } +}, { + headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation CreateToken( + $recipient: String! + $collection_id: BigInt! + $token_id: BigInt + $initial_supply: BigInt + $cap: TokenMintCapType! +) { + CreateToken( + recipient: $recipient + collectionId: $collection_id + params: { + tokenId: { integer: $token_id } + initialSupply: $initial_supply + cap: { type: $cap } + } + ) { + id + method + state + } +} +''' + +variables = { + 'recipient': 'cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f', #The recipient of the initial supply + 'collection_id': 2406, #Specify the collection ID + 'token_id': 0, #Specify the token ID + 'initial_supply': 1, #Mint initial supply + 'cap': 'INFINITE' #Define supply type +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +A WebSocket event will also be fired so you can pick up the changes in real-time by listening to the app channel on the WebSocket. + +:::tip +For Token ID management, head to [Best Practices > TokenID Structure](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md) +::: + +:::info Explore More Arguments +For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. +For instance, you'll find settings such as adding attributes/royalties/supply type and much more with the `CreateTokenParams` argument, or the ability to sign using a managed wallet with the `signingAccount` argument. +::: + +:::tip What's next? +To add metadata to your token, go to the [Adding Metadata](/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md) tutorial. +::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/_category_.json new file mode 100644 index 0000000..023c625 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Creating Tokens" +} + \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md new file mode 100644 index 0000000..0d4a079 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md @@ -0,0 +1,456 @@ +--- +title: "Adding Metadata" +slug: "adding-metadata" +description: "Learn how to create currency tokens using Enjin's platform, perfect for powering blockchain economies within games and applications." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Adding to a token enables games and apps to import detailed information and showcase your token's data. Usually, digital assets are identified just by its . Metadata adds extra details to these assets, giving them properties such as a title, a narrative, and visuals. + +:::info What you'll need: +- Some [ Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for and metadata s. +You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to add metadata to. +- Follow our [Best Practices for Collection/Token Metadata](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md). +::: + +You can even use metadata to attach gifs, videos, and 3D models to your token. This data can be stored both on-chain and off-chain to allow users and other supported platforms to access the information. + +When storing data on-chain, metadata can be defined as attributes such as name, description, fallback image, and any other attribute you choose. However, if the data is stored off-chain, it should always reference a centralized public repository through a .json file. + +## Metadata Storage + +There are three main ways to store metadata for your tokens: + +1. **On-Chain Storage: Attributes** + - Storing metadata directly on the blockchain is known as on-chain storage. + - It's immutable and highly secure since it's protected by the Enjin Blockchain's decentralized nature. + - On-chain storage can cost a small fee at the time of mint and has slight limitations in terms of space, making it suitable for smaller, permanent files. + +2. **Off-Chain Storage: JSON** + - This means the metadata is stored outside the blockchain on a separate server or database. + - It's often accessible through a URL, typically pointing to a JSON file containing the asset's information. + - Off-chain storage is cheaper and more flexible for large or frequently updated data, but it relies on external servers' availability and security. + +3. **InterPlanetary File System (): JSON** + - IPFS is a decentralized storage solution that distributes file storage across a network of computers. + - It allows metadata to be stored off the main blockchain but in a way that's still decentralized, tamper-proof, and permanent. + - IPFS assigns a unique hash to each file. When you store metadata on IPFS, you link to it using this hash, ensuring the data remains unchanged. + +Each option has trade-offs between cost, reliability, and security, and the choice depends on the specific needs and goals of the digital asset being created. + +## Attributes: OnChain Metadata + +You can assign multiple on-chain attributes to a token simultaneously, which allows you to define the metadata of the token. + +Some commonly recognized attributes, following the [Universal Metadata Standard](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md), include: + +- **`name`:** The title of the token. +- **`description`:** A brief explanation or description of the token. +- **`media`:** A token image, GIF, or MP4 to represent the token. +- **`URI`:** The web address (URL) where the JSON file containing the token's off-chain or IPFS metadata is hosted. + +:::tip Need to add more metadata? +Check the [Best Practices for Collection/Token Metadata](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md), and the [Universal Metadata Standard](https://enj.in/metadata). +::: + +:::warning Attributes for Collections and Tokens are very similar. +The process of adding attributes is similar for both collections and tokens. +While this tutorial guides you through adding an attribute to a token, you can follow the same steps to add attributes to a collection. +Simply navigate to the corresponding menu for collections instead of tokens. +::: + +:::warning **Important:** Attribute keys are case sensitive. +Ensure you use the correct casing when defining attributes to avoid errors. +::: + +**There are two ways to add metadata:** + +1. [Using the Enjin Dashboard](#option-a-using-the-enjin-dashboard) +2. [Using the GraphQL API & SDKs](#option-b-using-the-enjin-api--sdks) + +### Option A. Using the Enjin Dashboard + +In the Platform menu, navigate to "**[Tokens](https://platform.canary.enjin.io/tokens)**". +**Locate the token** you wish to add / edit attributes for, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Attributes**" button. + +![Adding Metadata](/img/guides/managing-tokens/adding-metadata.gif) + +:::tip Need to add multiple attributes for a token? +Click on the "**Batch**" button, followed by "**Batch SetAttribute**". +::: + +To add / edit an attribute, select the "**Set**" option, type in the "**Key**" that you wish to add / edit, and it's "**Value**" in the corresponding text fields. + +Once you're satisfied with the options, click on the "**Set Attribute**" button at the bottom right corner to create the request. + +

+ Set Attribute form +

+ +The Transaction Request will then appear in the "**Transactions**" menu. + +

+ Set Attribute Banner +

+ +![Pending Set Attribute Banner](/img/guides/managing-tokens/pending-set-attr-txn.png) + +Since this request requires a , it'll need to be signed with your Wallet. + +- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. +- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. + +### Option B. Using the Enjin API & SDKs + +The BatchSetAttribute mutation allows you to efficiently set or update multiple attributes for a specific token within a collection in a single blockchain transaction. Attributes represent various properties, characteristics, or metadata associated with a token. + + + +```graphql +mutation BatchSetAttribute { + BatchSetAttribute( + collectionId: 36105 # Specify the collection ID + tokenId: {integer: 0 } #Specify the token ID. If you wish to add collection metadata, omit this line entirely. + attributes: [ + { + key: "name" #Provide an attribute name + value: "Chronicles of the Celestium" #Provide an attribute value + }, + { + key: "description" #Provide an attribute name + value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." #Provide an attribute value + }, + { + key: "uri" #Provide an attribute name + value: "https://yourhost/metadata.json" #Provide an attribute value + } + ] + ) { + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation BatchSetAttribute($collection_id: BigInt!, $token_id: BigInt) {\r\n BatchSetAttribute(\r\n collectionId: $collection_id\r\n tokenId: { integer: $token_id }\r\n attributes: [\r\n { key: \"name\", value: \"Chronicles of the Celestium\" }\r\n {\r\n key: \"description\"\r\n value: \"An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse.\"\r\n }\r\n { key: \"uri\", value: \"https://yourhost/metadata.json\" }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":0}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +//Define attributes to set +var attributes = new List +{ + new AttributeInput() + .SetKey("name") //Provide an attribute name + .SetValue("Chronicles of the Celestium"), //Provide an attribute value + new AttributeInput() + .SetKey("description") //Provide an attribute name + .SetValue("An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse."), //Provide an attribute value + new AttributeInput() + .SetKey("uri") //Provide an attribute name + .SetValue("https://yourhost/metadata.json") //Provide an attribute value +}; + +// Setup the mutation +var batchSetAttribute = new BatchSetAttribute() + .SetCollectionId(36105) //Specify the collection ID + .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) //Specify the token ID. If you wish to add collection metadata, omit this line entirely. + .SetAttributes(attributes.ToArray()); //Set the previously defined attributes as an array + +// Define and assign the return data fragment to the mutation +var batchSetAttributeFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +batchSetAttribute.Fragment(batchSetAttributeFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendBatchSetAttribute(batchSetAttribute); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreMutations.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + //Define attributes to set + vector vectorAttributes; + AttributeInput nameAttribute1 = AttributeInput() + .SetKey(make_shared("name")) + .SetValue(make_shared("Chronicles of the Celestium")); + + AttributeInput nameAttribute2 = AttributeInput() + .SetKey(make_shared("description")) + .SetValue(make_shared("An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse.")); + + AttributeInput nameAttribute3 = AttributeInput() + .SetKey(make_shared("uri")) + .SetValue(make_shared("https://yourhost/metadata.json")); + + vectorAttributes.push_back(nameAttribute1); + vectorAttributes.push_back(nameAttribute2); + vectorAttributes.push_back(nameAttribute3); + + shared_ptr> attributes = make_shared>(vectorAttributes); + + // Setup mutation data + shared_ptr tokenId = make_shared(); + tokenId->SetInteger(make_shared("0")); + + // Setup mutation + BatchSetAttribute batchSetAttribute = BatchSetAttribute(); + batchSetAttribute + .SetCollectionId(make_shared("2406")) + .SetTokenId(tokenId) + .SetAttributes(attributes); + + // Define and assign the return data fragment to the mutation + shared_ptr transactionFragment = make_shared(); + transactionFragment + ->WithId() + .WithMethod() + .WithState(); + + batchSetAttribute.SetFragment(transactionFragment); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>>> futureResponse = SendBatchSetAttribute(*client, batchSetAttribute); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& transaction = gqlResult->GetData()->GetResult(); + + std::cout << to_string(transaction->GetId().value()) << std::endl; + std::cout << ToString(transaction->GetMethod().value()) << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation BatchSetAttribute($collection_id: BigInt!, $token_id: BigInt) { + BatchSetAttribute( + collectionId: $collection_id + tokenId: { integer: $token_id } + attributes: [ + { + key: "name" + value: "Chronicles of the Celestium" + } + { + key: "description" + value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." + } + { + key: "uri" + value: "https://yourhost/metadata.json" + } + ] + ) { + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + token_id: 0 //Specify the token ID. + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation BatchSetAttribute($collection_id: BigInt!, $token_id: BigInt) { + BatchSetAttribute( + collectionId: $collection_id + tokenId: { integer: $token_id } + attributes: [ + { key: "name", value: "Chronicles of the Celestium" } + { + key: "description" + value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." + } + { key: "uri", value: "https://yourhost/metadata.json" } + ] + ) { + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + token_id: 0 //Specify the token ID. + } +}, { + headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation BatchSetAttribute($collection_id: BigInt!, $token_id: BigInt) { + BatchSetAttribute( + collectionId: $collection_id + tokenId: { integer: $token_id } + attributes: [ + { key: "name", value: "Chronicles of the Celestium" } + { + key: "description" + value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." + } + { key: "uri", value: "https://yourhost/metadata.json" } + ] + ) { + id + method + state + } +} +''' + +variables = { + 'collection_id': 36105, #Specify the collection ID + 'token_id': 0, #Specify the token ID. +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +A WebSocket event will also be fired so you can pick up the changes in real-time by listening to the app channel on the WebSocket. + +## JSON File: Off-Chain & IPFS Metadata + +A JSON file can be used to provide comprehensive information about the asset, including its description, visual representation, multimedia attachments, external references, categorization, associated files, and specific attributes. + +Take a look at this simple JSON schema (following the [Universal Metadata Standard](https://enj.in/metadata)) to learn how to properly format the off-chain metadata for your NFT. + +```json +{ + "name": "Starforged Cleaver", + "description": "A blade smithed from cosmic steel, its edge sharp enough to slice through the fabric of reality itself.", + "external_url": "https://enjin.io", + "fallback_image": "https://yourhost/image.jpg", + "media": [ + { + "type": "image/png", + "url": "https://yourhost/image.jpg" + } + ], + "attributes": { + "Strength": { + "value": "800" + } + } +} +``` + +Take note of these data points: + +- **`description`**: This field provides a human-readable description of the asset. It's a text description that helps users understand the nature or significance of the asset. + +- **`media`**: The `media` field contains a URL pointing to an image representation of the asset. It supports PNG, GIF, and JPG file formats. + +- **`external_url`**: The "external_url" field is a URL pointing to an external application or website where users can view additional information or interact with the asset. + +- **`attributes`**: This is an object array where each object should contain an `title` and `value` fields. The attribute `title` defines the type of trait or attribute associated with the asset, while `value` can be a string or a number, representing the value of that trait or attribute. + +You can also host your JSON files on IPFS, to make your metadata more immutable. + +:::tip Need to add more metadata? +Check the [Best Practices for Collection/Token Metadata](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md), and the [Universal Metadata Standard](https://enj.in/metadata). +::: + +:::info Explore More Arguments +For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. + +For instance, you'll find settings such as `continueOnFailure` to skip data that would cause the whole batch to fail, or the ability to sign using a managed wallet with the `signingAccount` argument. +::: + +:::tip What's next? +To mint some token supply, head to the [Minting Tokens](/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md) tutorial. +::: \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md new file mode 100644 index 0000000..9f33619 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md @@ -0,0 +1,339 @@ +--- +title: "Minting Tokens" +slug: "minting-a-token" +description: "Enhance your blockchain assets by adding custom metadata, providing additional details and functionalities to your tokens." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Now that you've got your tokens created, it's time to start minting them into player wallets as they are earned. + +We call this "Play-to-Mint," giving players the power to create tokens themselves, which feels more rewarding. + +Plus, it keeps your processes more efficient by delivering tokens right to players' wallets without any unnecessary `transfer` transactions. + +:::info What you'll need: +- Some [ Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for and a deposit of 0.01 ENJ is required for the , for each new token holder. +If the token has , each new unit minted will require the same amount of ENJ to be infused. +You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to mint. +::: + +**There are two ways to use the functionalities:** + +1. [Using the Enjin Dashboard](#option-a-using-the-enjin-dashboard) +2. [Using the GraphQL API & SDKs](#option-b-using-the-enjin-api--sdks) + +## Option A. Using the Enjin Dashboard + +In the Platform menu, navigate to "**[Tokens](https://platform.canary.enjin.io/tokens)**". +**Locate the token** you wish to mint, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Mint**" button. + +![Minting a Token](/img/guides/managing-tokens/minting-a-token.gif) + +Set the recipient and the amount in the corresponding fields, and Click on "**Mint**" + +

+ Mint Token Form +

+ +The Transaction Request will then appear in the "**Transactions**" menu. + +

+ Mint Transaction Request Banner +

+ +![Pending Mint Transaction](/img/guides/managing-tokens/pending-mint-txn.png) + +Since this request requires a , it'll need to be signed with your Wallet. + +- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. +- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. + +## Option B. Using the Enjin API & SDKs + +The BatchMint mutation enables you to efficiently create multiple tokens within a single blockchain transaction. This process, known as batch minting, simplifies the minting of multiple tokens, reducing transaction fees and processing time. + + + +```graphql +mutation BatchMint { + BatchMint( + collectionId: "7154" #Specify the collection ID + recipients: [ + { + account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" #The recipient of the mint + mintParams: { + amount:1 #Amount to mint + tokenId: {integer: 6533} #Token ID to mint + } + } + ] + ) { + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation BatchMint($collection_id: BigInt!) {\r\n BatchMint(\r\n collectionId: $collection_id\r\n recipients: [\r\n {\r\n account: \"0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921\"\r\n mintParams: {\r\n amount: 1\r\n tokenId: { integer: 6533 }\r\n }\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"collection_id":7154}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Define the list of recipients and their mint parameters +var recipients = new List +{ + new MintRecipient() + .SetAccount("0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921") + .SetMintParams(new MintTokenParams() + .SetAmount(1) + .SetTokenId(new EncodableTokenIdInput().SetInteger(6533)) + ) +}; + +// Setup the mutation +var batchMint = new BatchMint() + .SetCollectionId(7154) + .SetRecipients(recipients.ToArray()); + +// Define and assign the return data fragment to the mutation +var batchMintFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +batchMint.Fragment(batchMintFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendBatchMint(batchMint); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreMutations.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Define the list of recipients and their mint parameters + shared_ptr tokenId = make_shared(); + tokenId->SetInteger(make_shared("0")); + + MintTokenParams mintTokenParams = MintTokenParams() + .SetAmount(make_shared("1")) + .SetTokenId(tokenId); + + + MintRecipient mintRecipient = MintRecipient() + .SetAccount(make_shared("0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921")) + .SetMintParams(make_shared(mintTokenParams)); + + vector recipients; + recipients.push_back(mintRecipient); + + // Setup mutation + BatchMint batchMint = BatchMint() + .SetCollectionId(make_shared("7154")) + .SetRecipients(make_shared>(recipients)); + + // Define and assign the return data fragment to the mutation + shared_ptr transactionFragment = make_shared(); + transactionFragment + ->WithId() + .WithMethod() + .WithState(); + + batchMint.SetFragment(transactionFragment); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>>> futureResponse = SendBatchMint(*client, batchMint); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& transaction = gqlResult->GetData()->GetResult(); + + std::cout << to_string(transaction->GetId().value()) << std::endl; + std::cout << ToString(transaction->GetMethod().value()) << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation BatchMint( + $collection_id: BigInt! + ) { + BatchMint( + collectionId: $collection_id + recipients: [ + { + account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" #The recipient of the mint + mintParams: { + amount:1 #Amount to mint + tokenId: {integer: 6533} #Token ID to mint + } + } + ] + ) { + id + method + state + } + } + `, + variables: { + collection_id: 7154 //Specify the collection ID + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation BatchMint( + $collection_id: BigInt! + ) { + BatchMint( + collectionId: $collection_id + recipients: [ + { + account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" #The recipient of the mint + mintParams: { + amount:1 #Amount to mint + tokenId: {integer: 6533} #Token ID to mint + } + } + ] + ) { + id + method + state + } + } + `, + variables: { + collection_id: 7154 //Specify the collection ID + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation BatchMint( + $collection_id: BigInt! +) { + BatchMint( + collectionId: $collection_id + recipients: [ + { + account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" #The recipient of the mint + mintParams: { + amount:1 #Amount to mint + tokenId: {integer: 6533} #Token ID to mint + } + } + ] + ) { + id + method + state + } +} +''' + +variables = { + 'collection_id': 7154 #Specify the collection ID +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +A WebSocket event will also be fired so you can pick up the transfer transaction in real time by listening to the app channel on the WebSocket. + +:::info Explore More Arguments +For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. + +For instance, you'll find settings such as the ability to sign using a managed wallet with the `signingAccount` argument, or batch create tokens instead of minting existing ones with the `createParams` argument. +::: + +:::tip You've minted a token! +What if you need to transfer a token? proceed to [Transferring Tokens](/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md). +::: \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md new file mode 100644 index 0000000..e249128 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md @@ -0,0 +1,724 @@ +--- +title: "Transferring Tokens" +slug: "transferring-tokens" +description: "Find out how to securely transfer tokens between users or platforms using the Enjin Platform, ensuring safe and efficient asset movement." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +You will need to transfer tokens for: + +- Implementing various gameplay features such as gifting, trading, and rewards. +- Supporting secure and efficient token transactions. +- Providing a seamless user experience by allowing token transfers without leaving the game environment. + +:::info What you'll need: +- Some [ Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for and a deposit of 0.01 ENJ is required for the , for each new token holder. +You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to mint. +::: + +**There are two ways to transfer a token:** + +1. [Using the Enjin Dashboard](#option-a-using-the-enjin-dashboard) +2. [Using the GraphQL API & SDKs](#option-b-using-the-enjin-api--sdks) + +## Option A. Using the Enjin Dashboard + +In the Platform menu, navigate to "**[Tokens](https://platform.canary.enjin.io/tokens)**". +**Locate the token** you wish to transfer, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Transfer**" button. + +![Transferring Token](/img/guides/managing-tokens/transferring-token.gif) + +:::info Need to perform multiple transfers? +Click on the "**Batch**" button, followed by "**Batch Transfer**". +::: + +Fill in the recipient, amount, and other optional arguments in the corresponding fields. +Once you're satisfied with the options, click on the "**Transfer**" button at the bottom right corner to create the request. + +

+ Transfer Token Form +

+ +

+ Transfer Token Transaction Banner +

+ +![Pending Transfer Transaction](/img/guides/managing-tokens/pending-transfer-txn.png) +Clicking "**View**" on the notification will take you to your Transactions List. + +Since this request requires a , it'll need to be signed with your Wallet. + +- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. +- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. + +If you're looking to distribute tokens to your community or players, but don't have their account addresses, don't worry! Our solution is Enjin Beam. +Proceed to the [Distributing Tokens via QR](/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md) tutorial to learn more. + +## Option B. Using the Enjin API & SDKs + +### Transferring an asset + +`SimpleTransferToken` mutation simplifies the process of transferring a specific token from one wallet to another. It is a straightforward way to facilitate token transfers without the need for complex intermediary steps. + + + +```graphql +mutation TransferToken{ + SimpleTransferToken( + collectionId: 36105 #Specify the collection ID + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the recipent address + params: { + tokenId: {integer: 0} #Specify the token ID + amount: 1 #Choose the transfer amount + } + ){ + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation TransferToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $recipient: String!\r\n $amount: BigInt!\r\n) {\r\n SimpleTransferToken(\r\n collectionId: $collection_id\r\n recipient: $recipient\r\n params: { tokenId: { integer: $token_id }, amount: $amount }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"collection_id":36105,"token_id":0,"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","amount":1}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Define the simple transfer parameters +var simpleTransferParams = new SimpleTransferParams() + .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) + .SetAmount(1); + + +// Setup the mutation +var simpleTransferToken = new SimpleTransferToken() + .SetRecipient("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f") + .SetCollectionId(36105) + .SetParams(simpleTransferParams); + +// Define and assign the return data fragment to the mutation +var simpleTransferTokenFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +simpleTransferToken.Fragment(simpleTransferTokenFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendSimpleTransferToken(simpleTransferToken); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreMutations.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Define the simple transfer params + shared_ptr tokenId = make_shared(); + tokenId->SetInteger(make_shared("0")); + + SimpleTransferParams simpleTransferParams = SimpleTransferParams() + .SetAmount(make_shared("1")) + .SetTokenId(tokenId); + + // Setup mutation + SimpleTransferToken simpleTransferToken = SimpleTransferToken() + .SetRecipient(make_shared("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f")) + .SetCollectionId(make_shared("36105")) + .SetParams(make_shared(simpleTransferParams)); + + // Define and assign the return data fragment to the mutation + shared_ptr transactionFragment = make_shared(); + transactionFragment + ->WithId() + .WithMethod() + .WithState(); + + simpleTransferToken.SetFragment(transactionFragment); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>>> futureResponse = SendSimpleTransferToken(*client, simpleTransferToken); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& transaction = gqlResult->GetData()->GetResult(); + + std::cout << to_string(transaction->GetId().value()) << std::endl; + std::cout << ToString(transaction->GetMethod().value()) << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} + +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation TransferToken( + $collection_id: BigInt! + $token_id: BigInt! + $recipient: String! + $amount: BigInt! + ) { + SimpleTransferToken( + collectionId: $collection_id + recipient: $recipient + params: { + tokenId: {integer: $token_id} + amount: $amount + } + ){ + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + token_id: 0, //Specify the token ID + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address + amount: 1 //Choose the transfer amount + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation TransferToken( + $collection_id: BigInt! + $token_id: BigInt! + $recipient: String! + $amount: BigInt! + ) { + SimpleTransferToken( + collectionId: $collection_id + recipient: $recipient + params: { + tokenId: {integer: $token_id} + amount: $amount + } + ){ + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + token_id: 0, //Specify the token ID + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address + amount: 1 //Choose the transfer amount + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation TransferToken( + $collection_id: BigInt! + $token_id: BigInt! + $recipient: String! + $amount: BigInt! +) { + SimpleTransferToken( + collectionId: $collection_id + recipient: $recipient + params: { + tokenId: {integer: $token_id} + amount: $amount + } + ){ + id + method + state + } +} +''' + +variables = { + 'collection_id': 36105, #Specify the collection ID + 'token_id': 0, #Specify the token ID + 'recipient': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", #Specify the recipent address + 'amount': 1 #Choose the transfer amount +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +### Transferring ENJ token + +To send ENJ / CENJ tokens from one wallet to another, use the `TransferAllowDeath` mutation, or the `TransferKeepAlive` if you want to make sure the account doesn't get reaped ([Read more about account reaping](https://support.enjin.io/hc/en-gb/articles/16297132519569-What-is-the-Existential-Deposit)): + + + +```graphql +mutation TransferENJTokens { + TransferKeepAlive( + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the recipent address + amount: 5000000000000000000 #Specify the amount of tokens to transfer + ) { + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation TransferENJTokens(\r\n $recipient: String!\r\n $amount: BigInt!\r\n $keep_alive: Boolean\r\n) {\r\n TransferBalance(\r\n recipient: $recipient\r\n amount: $amount\r\n keepAlive: $keep_alive\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","amount":1,"keep_alive":true}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Set up the mutation +var transferKeepAlive = new TransferKeepAlive() + .SetRecipient("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f") //The recipient of the initial supply + .SetAmount(5000000000000000000); //The amount of tokens to transfer + +// Define and assign the return data fragment to the mutation +var transferKeepAliveFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +transferKeepAlive.Fragment(transferKeepAliveFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendTransferKeepAlive(transferKeepAlive); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +Work In Progress +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation TransferKeepAlive( + $recipient: String! + $amount: BigInt! + ) { + TransferBalance( + recipient: $recipient + amount: $amount + ){ + id + method + state + } + } + `, + variables: { + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address + amount: 1 //Specify the amount of tokens to transfer + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation TransferKeepAlive( + $recipient: String! + $amount: BigInt! + ) { + TransferBalance( + recipient: $recipient + amount: $amount + ){ + id + method + state + } + } + `, + variables: { + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address + amount: 1 //Specify the amount of tokens to transfer + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation TransferKeepAlive( + $recipient: String! + $amount: BigInt! +) { + TransferBalance( + recipient: $recipient + amount: $amount + ){ + id + method + state + } +} +''' + +variables = { + 'recipient': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", #Specify the recipent address + 'amount': 1 #Specify the amount of tokens to transfer + +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +:::info **Key Arguments** +- **Calculating the `amount`** + - The Platform accepts ENJ values in the **base unit** (integers), not decimal ENJ amounts. To calculate the correct input, **multiply your desired ENJ amount by 10^18** (1 quintillion). + - **Formula:** `Desired ENJ` \* `1,000,000,000,000,000,000` + - **Example:** To transfer **5 ENJ**, input `5000000000000000000`. +- **Using `keepAlive`** + - Set to `true` to prevent the account from being removed (reaped) if the balance drops below the minimum requirement. + - [Learn more about the keepAlive argument here](/03-api-reference/04-important-arguments.md#keepalive) +::: + +### Batch Transferring ENJ token + +To send ENJ / CENJ tokens from one wallet to multiple addresses or in multiple transactions, we use the `BatchTransferBalance` mutation + + + +```graphql +mutation BatchSendENJ{ + BatchTransferBalance( + recipients: [ + { + account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", #Specify the recipent address + transferBalanceParams: { + value: 5000000000000000000 #Specify the amount of tokens to transfer + } + }, + { + account: "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", #Specify the recipent address + transferBalanceParams: { + value: 15250000000000000000 #Specify the amount of tokens to transfer + keepAlive: true #Set to true if you want to make sure the account doesn't get reaped + } + }, + ] + ){ + id + transactionId + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation BatchSendENJ($recipients: [TransferRecipient!]!) {\r\n BatchTransferBalance(recipients: $recipients) {\r\n id\r\n transactionId\r\n state\r\n }\r\n}\r\n","variables":{"recipients":[{"account":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","transferBalanceParams":{"value":5000000000000000000}},{"account":"cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu","transferBalanceParams":{"value":15250000000000000000,"keepAlive":true}}]}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Set up the transfers for the batch +var transferBalanceParams1 = new TransferBalanceParams() + .SetValue(5000000000000000000); //The amount of tokens to transfer + +var transferBalanceParams2 = new TransferBalanceParams() + .SetValue(5000000000000000000) //The amount of tokens to transfer + .SetKeepAlive(true); //Whether the transaction will be kept from failing if the balance drops below the minimum requirement + +var transferRecipients = new[] +{ + new TransferRecipient() + .SetAccount("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f") + .SetTransferBalanceParams(transferBalanceParams1), + new TransferRecipient() + .SetAccount("cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu") + .SetTransferBalanceParams(transferBalanceParams2) +}; + +// Set up the mutation +var batchTransferBalance = new BatchTransferBalance() + .SetRecipients(transferRecipients); + +// Define and assign the return data fragment to the mutation +var transferBalanceFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +batchTransferBalance.Fragment(transferBalanceFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendBatchTransferBalance(batchTransferBalance); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +Work In Progress +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation BatchSendENJ($recipients: [TransferRecipient!]!){ + BatchTransferBalance( + recipients: $recipients + ){ + id + transactionId + state + } + } + `, + variables: { + recipients:[ + { + account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address + transferBalanceParams: { value: 5000000000000000000 } //Specify the amount of tokens to transfer + }, + { + account: "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", //Specify the recipent address + transferBalanceParams: { + value: 15250000000000000000, //Specify the amount of tokens to transfer + keepAlive: true //Set to true if you want to make sure the account doesn't get reaped + } + } + ] + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation BatchSendENJ($recipients: [TransferRecipient!]!){ + BatchTransferBalance( + recipients: $recipients + ){ + id + transactionId + state + } + } + `, + variables: { + recipients:[ + { + account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address + transferBalanceParams: { value: 5000000000000000000 } //Specify the amount of tokens to transfer + }, + { + account: "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", //Specify the recipent address + transferBalanceParams: { + value: 15250000000000000000, //Specify the amount of tokens to transfer + keepAlive: true //Set to true if you want to make sure the account doesn't get reaped + } + } + ] + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation BatchSendENJ($recipients: [TransferRecipient!]!){ + BatchTransferBalance( + recipients: $recipients + ){ + id + transactionId + state + } +} +''' + +variables = { + 'recipients':[ + { + 'account': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", #Specify the recipent address + 'transferBalanceParams': { 'value': 5000000000000000000 } #Specify the amount of tokens to transfer + }, + { + 'account': "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", #Specify the recipent address + 'transferBalanceParams': { + 'value': 15250000000000000000, #Specify the amount of tokens to transfer + 'keepAlive': True #Set to true if you want to make sure the account doesn't get reaped + } + } + ] +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +A WebSocket event will also be fired so you can pick up the transfer transaction in real time by listening to the app channel on the WebSocket. + +:::tip Need to send a transaction request to user's wallet? +This can be done using Enjin Platform API & WalletConnect! +To learn more, check out the [Using WalletConnect page](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md). +::: + +:::info Explore More Arguments +For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. +For instance, you'll find settings such as `continueOnFailure` to skip data that would cause the whole batch to fail, or the ability to sign using a managed wallet with the `signingAccount` argument. +::: + +:::What's next? +Distribute tokens to your players, even if they don't have wallets! +Proceed to the [Distributing Tokens via QR](/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md) tutorial to learn more. +::: \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md new file mode 100644 index 0000000..306a541 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md @@ -0,0 +1,995 @@ +--- +title: "Create QR Drops" +slug: "create-qr-drops" +description: "Discover how to create QR drops, allowing for simple distribution of blockchain tokens to users via scannable QR codes." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + is a simple way to distribute digital assets like tokens and collectibles using QR codes. Users just need to scan a Beam QR code with their Enjin Wallet to receive tokens directly. It's a great tool for marketing campaigns, events, or promotions to give away items and easily bring new users into your ecosystem. + +:::info What you'll need: +- Some [ Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for and a deposit of 0.01 ENJ is required for the , for each new token holder. +You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to mint. +::: + +**There are two ways to create an Enjin Beam:** + +1. [Using the Enjin Dashboard](#option-a-using-the-enjin-dashboard) +2. [Using the Enjin API & SDKs](#option-b-using-the-enjin-api--sdks) + +## Option A. Using the Enjin Dashboard + +1. Log in to your Enjin Platform account. +2. In the main menu, go to "**Beams**" ([https://platform.canary.enjin.io/beams/list](https://platform.canary.enjin.io/beams/list)). +3. Click the "**Create Enjin Beam**" button ([https://platform.canary.enjin.io/create/beam](https://platform.canary.enjin.io/create/beam)). + +![Creating a Beam](/img/guides/managing-tokens/create-beam.gif) + +Now, you'll fill out the details for your Beam: + +- **Beam Settings:** Configure the basic properties of your Beam. + - **Collection ID:** Select the collection that contains the tokens you want to distribute. + - **Beam Metadata:** Give your Beam a name, description, and add an image. This is what users will see when they scan the QR code. + +

+ Create Beam Form +

+ +- **Tokens to Distribute:** This is where you define which tokens are given out and how many. + - A single Beam is made up of one or more "Beam Claims". Each time a user scans the QR code and successfully receives a token, they are redeeming one **Beam Claim**. + - **[Token IDs](/03-api-reference/04-important-arguments.md#tokenid):** Specify the IDs of the specific tokens you want to distribute with this claim. + - **[Claim Quantity](/03-api-reference/04-important-arguments.md#claimquantity):** This sets how many individual Beam Claims you want to create for each selected Token ID. + - **[Token Quantity Per Claim](/03-api-reference/04-important-arguments.md#tokenquantityperclaim):** This sets how many units of the token a user receives *each time* they redeem a single Beam Claim. For example, if you're giving out stacks of 5 gold coins with each claim, set this to 5. + - **[Type](/03-api-reference/04-important-arguments.md#beam-claim-type):** Choose whether redeeming a claim will `MINT` a brand new token or `TRANSFER` an existing token from your account balance. + +

+ Create Beam Form 2 +

+ +:::tip Use **[Flags](/03-api-reference/04-important-arguments.md#flags)** for advanced options and more control + - **Single-use:** This important flag changes how the QR code works. + - **Default (Single-use OFF):** You get one main QR code for the entire Beam. When a user scans it, the system randomly picks one of the available Beam Claims to give them. + - **Single-use ON:** A *unique QR code* is generated for *every single Beam Claim*. + - **Why use Single-use?** It gives you precise control over which specific claim (and thus which token) goes to which person. It's also more secure for private distributions; you can share individual QR codes with specific users without worrying about a main QR code being leaked publicly. +::: + +Once you're satisfied with the options, click on the "**Create**" button at the bottom right corner to create the request. +If a signature request is needed, approve it. + +Once the Beam is created, locate it in the "**Beams**" menu, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Claim**" button to view the QR code. + +![Claiming a Beam](/img/guides/managing-tokens/claim-beam.gif) + +:::warning Need to get all of the "Single-Use" QR codes? +Please note, viewing all of the "Single-Use" QR codes is currently not available in the Platform User Interface. +For the time being, it can be viewed programmatically only via [Enjin API & SDKs](#option-b-using-the-enjin-api--sdks) +::: + +The Enjin Beam is now ready to be shared to distribute tokens. Users can scan the QR code and instantly receive your token! +Next skillset to learn is mutating a collection / token. +Proceed to the [Freezing & Thawing](/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md) tutorial to learn more. + +## Option B. Using the Enjin API & SDKs + +:::info Make sure to use the Beam Endpoint +- Testnet: `http://platform.canary.enjin.io/graphql/beam` +- Mainnet: `http://platform.enjin.io/graphql/beam` + +**Try the Beam Playground here:** +- Testnet: http://platform.canary.enjin.io/graphiql/beam +- Mainnet: http://platform.enjin.io/graphiql/beam +::: + +First, create the Beam with the parameters of your choice, such as name, description, image, begin and end date & the number of assets that you would like to distribute. + +**Mutation:** + + + +```graphql +mutation CreateBeam { + CreateBeam( + name: "My Beam" #Specify beam name + description: "This is a test beam." #Specify beam description + image: "https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg" #Specify url for beam image - shows during claim process + start: "1st January 2023" #Specify start date + end: "1st January 2024" #Specify end date + collectionId: 36623 #Select the collection ID + tokens: [{ + tokenIds: ["1..10", "123"] #Select the Token IDs. Use comma for multiple tokenIds: ["123", "234"], 2 dots for range: ["123", "234..345"] + tokenQuantityPerClaim:1 #Specify the number of tokens a user can receive per claim + claimQuantity:1 #Specify the number of times a user can claim + type:MINT_ON_DEMAND #Select token distribution type + }] + ) +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation CreateBeam(\r\n $name: String!\r\n $description: String!\r\n $image: String!\r\n $start: DateTime!\r\n $end: DateTime!\r\n $collection_id: BigInt!\r\n) {\r\n CreateBeam(\r\n name: $name\r\n description: $description\r\n image: $image\r\n start: $start\r\n end: $end\r\n collectionId: $collection_id\r\n tokens: [\r\n {\r\n tokenIds: [\"1..10\", \"123\"]\r\n tokenQuantityPerClaim: 1\r\n claimQuantity: 1\r\n type: MINT_ON_DEMAND\r\n }\r\n ]\r\n )\r\n}\r\n","variables":{"name":"My Beam","description":"This is a test beam.","image":"https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg","start":"1st January 2023","end":"1st January 2024","collection_id":36623}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; +using Enjin.Platform.Sdk.Beam; + +Cl +var tokens = new List +{ + new ClaimToken() + .SetTokenIds(new List + { + new IntegerRange(1, 10), + new IntegerRange(123) + }.ToArray() + ) + .SetClaimQuantity(1) + .SetTokenQuantityPerClaim(1) + .SetType(BeamType.MintOnDemand) +}; + +// Setup the mutation +var createBeam = new CreateBeam() + .SetName("My Beam") + .SetDescription("This is a test beam.") + .SetImage("https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg") + .SetStart(new DateTime(2023, 1, 1)) + .SetEnd(new DateTime(2024, 1, 1)) + .SetCollectionId(36623) + .SetTokens(tokens.ToArray()); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendCreateBeam(createBeam); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdkBeam/BeamMutations.hpp" +#include "EnjinPlatformSdkBeam/Beam.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace enjin::platform::sdk::beam; +using namespace std; + +int main() { + + // Define the list of tokens and claim parameters to include in the Beam + vector claimTokens; + vector integerRanges; + integerRanges.emplace_back("1", "10"); + integerRanges.emplace_back("123"); + + ClaimToken claimToken1 = ClaimToken() + .SetTokenIds(make_shared>(integerRanges)) + .SetClaimQuantity(make_shared(1)) + .SetTokenQuantityPerClaim(make_shared(1)) + .SetType(BeamType::MintOnDemand); + + claimTokens.push_back(claimToken1); + + DateTime startAt = DateTime::Parse("2023-01-01T00:00:00+0"); // Note the date string should be in ISO 8601 format. + DateTime endAt = DateTime::Parse("2024-01-01T00:00:00+0"); // Note the date string should be in ISO 8601 format. + + // Setup mutation + CreateBeam createBeam = CreateBeam() + .SetName(make_shared("My Beam")) + .SetDescription(make_shared("This is a test beam.")) + .SetImage(make_shared("https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg")) + .SetStart(make_shared(startAt)) + .SetEnd(make_shared(endAt)) + .SetCollectionId(make_shared("36623")) + .SetTokens(make_shared>(claimTokens)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendCreateBeam(*client, createBeam); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& beamCode = gqlResult->GetData()->GetResult(); + + std::cout << beamCode.value() << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation CreateBeam + ( + $name: String! + $description: String! + $image: String! + $start: DateTime! + $end: DateTime! + $collection_id: BigInt! + ) { + CreateBeam( + name: $name + description: $description + image: $image + start: $start + end: $end + collectionId: $collection_id + tokens: [{ + tokenIds: ["1..10", "123"] #Select the Token IDs. Use comma for multiple tokenIds ["123", "234"], 2 dots for range: ["123", "234..345"] + tokenQuantityPerClaim:1 #Specify the number of tokens a user can receive per claim + claimQuantity:1 #Specify the number of times a user can claim + type:MINT_ON_DEMAND #Select token distribution type + }] + ) + } + `, + variables: { + name: "My Beam", //Specify beam name + description: "This is a test beam.", //Specify beam description + image: "https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg", //Specify url for beam image - shows during claim process + start: "1st January 2023", //Specify start date + end: "1st January 2024", //Specify end date + collection_id: 36623 //Select the collection ID + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation CreateBeam + ( + $name: String! + $description: String! + $image: String! + $start: DateTime! + $end: DateTime! + $collection_id: BigInt! + ) { + CreateBeam( + name: $name + description: $description + image: $image + start: $start + end: $end + collectionId: $collection_id + tokens: [{ + tokenIds: ["1..10" ,"123"] #Select the Token IDs. Use comma for multiple tokenIds: ["123", "234"], 2 dots for range: ["123", "234..345"] + tokenQuantityPerClaim:1 #Specify the number of tokens a user can receive per claim + claimQuantity:1 #Specify the number of times a user can claim + type:MINT_ON_DEMAND #Select token distribution type + }] + ) + } + `, + variables: { + name: "My Beam", //Specify beam name + description: "This is a test beam.", //Specify beam description + image: "https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg", //Specify url for beam image - shows during claim process + start: "1st January 2023", //Specify start date + end: "1st January 2024", //Specify end date + collection_id: 36623 //Select the collection ID + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation CreateBeam +( + $name: String! + $description: String! + $image: String! + $start: DateTime! + $end: DateTime! + $collection_id: BigInt! +) { + CreateBeam( + name: $name + description: $description + image: $image + start: $start + end: $end + collectionId: $collection_id + tokens: [{ + tokenIds: ["1..10", "123"] #Select the Token IDs. Use comma for multiple tokenIds: ["123", "234"], 2 dots for range: ["123", "234..345"] + tokenQuantityPerClaim:1 #Specify the number of tokens a user can receive per claim + claimQuantity:1 #Specify the number of times a user can claim + type:MINT_ON_DEMAND #Select token distribution type + }] + ) +} +''' + +variables = { + 'name': "My Beam", #Specify beam name + 'description': "This is a test beam.", #Specify beam description + 'image': "https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg", #Specify url for beam image - shows during claim process + 'start': "1st January 2023", #Specify start date + 'end': "1st January 2024", #Specify end date + 'collection_id': 36623 #Select the collection ID +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +Next you will need to send a query to get one of the two different code types. + +- **Multi-Use Codes:** Useful for advertising. +- **Single-Use Codes:** Useful for rewarding individuals. + +**Query: Multi-Use Codes** + + + +```graphql +query GetBeam { + GetBeam(code: "adab7ac8bda71c2e2bf8a3228f0a435f") { #Set the beam code + id + code + name + description + image + start + end + isClaimable + claimsRemaining + qr { + url + payload + } + collection { + collectionId + maxTokenCount + maxTokenSupply + forceSingleMint + frozen + network + } + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"query GetBeam($code: String!) {\r\n GetBeam(code: $code) {\r\n id\r\n code\r\n name\r\n description\r\n image\r\n start\r\n end\r\n isClaimable\r\n claimsRemaining\r\n qr {\r\n url\r\n payload\r\n }\r\n collection {\r\n collectionId\r\n maxTokenCount\r\n maxTokenSupply\r\n forceSingleMint\r\n frozen\r\n network\r\n }\r\n }\r\n}\r\n","variables":{"code":"adab7ac8bda71c2e2bf8a3228f0a435f"}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; +using Enjin.Platform.Sdk.Beam; + +// Setup the query +var getBeam = new GetBeam() + .SetCode("adab7ac8bda71c2e2bf8a3228f0a435f"); + +// Define and assign the return data fragment to the query +var beamFragment = new BeamFragment() + .WithId() + .WithCode()Wi + .WithName() + .WithDescription() + .WithImage() + .WithStart() + .WithEnd() + .WithQr(new BeamQrFragment() + .WithUrl() + .WithPayload() + ) + .WithCollection(new CollectionFragment() + .WithCollectionId() + .WithMaxTokenCount() + .WithMaxTokenSupply() + .WithForceSingleMint() + .WithFrozen() + .WithNetwork() + ); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendGetBeam(getBeam); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdkBeam/BeamQueries.hpp" +#include "EnjinPlatformSdkBeam/Beam.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace enjin::platform::sdk::beam; +using namespace std; + +int main() { + + // Set up the query + GetBeam getBeam = GetBeam() + .SetCode(make_shared("adab7ac8bda71c2e2bf8a3228f0a435f")); + + // Define the fragment for the returned data + BeamFragment beamFragment = BeamFragment() + .WithId() + .WithCode() + .WithName() + .WithDescription() + .WithImage() + .WithStart() + .WithEnd() + .WithQr(make_shared( + BeamQrFragment() + .WithUrl() + .WithPayload() + ) + ) + .WithCollection(make_shared( + CollectionFragment() + .WithCollectionId() + .WithMaxTokenCount() + .WithMaxTokenSupply() + .WithForceSingleMint() + .WithFrozen() + .WithNetwork() + ) + ); + + getBeam.SetFragment(make_shared(beamFragment)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendGetBeam(*client, getBeam); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& beam = gqlResult->GetData()->GetResult(); + + std::cout << beam->GetCode().value() << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + query GetBeam + ( + $code: String! + ) { + GetBeam(code: $code) { + id + code + name + description + image + start + end + isClaimable + claimsRemaining + qr { + url + payload + } + collection { + collectionId + maxTokenCount + maxTokenSupply + forceSingleMint + frozen + network + } + } + } + `, + variables: { + code: "adab7ac8bda71c2e2bf8a3228f0a435f" //Set the beam code + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + query GetBeam + ( + $code: String! + ) { + GetBeam(code: $code) { + id + code + name + description + image + start + end + isClaimable + claimsRemaining + qr { + url + payload + } + collection { + collectionId + maxTokenCount + maxTokenSupply + forceSingleMint + frozen + network + } + } + } + `, + variables: { + code: "adab7ac8bda71c2e2bf8a3228f0a435f" //Set the beam code + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +query GetBeam +( + $code: String! +) { + GetBeam(code: $code) { + id + code + name + description + image + start + end + isClaimable + claimsRemaining + qr { + url + payload + } + collection { + collectionId + maxTokenCount + maxTokenSupply + forceSingleMint + frozen + network + } + } +} +''' + +variables = { + 'code': "adab7ac8bda71c2e2bf8a3228f0a435f" #Set the beam code +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +**Query: Single-Use Codes** + + + +```graphql +query GetSingleUseCodes { + GetSingleUseCodes(code: "adab7ac8bda71c2e2bf8a3228f0a435f") { #Set the beam code + edges { + cursor + node { + qr { + url + } + } + } + totalCount + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"query GetSingleUseCodes($code: String!) {\r\n GetSingleUseCodes(code: $code) {\r\n edges {\r\n cursor\r\n node {\r\n qr {\r\n url\r\n }\r\n }\r\n }\r\n totalCount\r\n pageInfo {\r\n hasPreviousPage\r\n hasNextPage\r\n startCursor\r\n endCursor\r\n }\r\n }\r\n}\r\n","variables":{"code":"adab7ac8bda71c2e2bf8a3228f0a435f"}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; +using Enjin.Platform.Sdk.Beam; + +// Setup the query +var getSingleUseCodes = new GetSingleUseCodes() + .SetCode("adab7ac8bda71c2e2bf8a3228f0a435f"); + +// Define and assign the return data fragment to the query +var beamConnectionFragment = new ConnectionFragment() + .WithEdges(new EdgeFragment() + .WithCursor() + .WithNode(new BeamClaimFragment() + .WithQr( + new BeamQrFragment() + .WithUrl() + ) + ) + ) + .WithTotalCount() + .WithPageInfo(new PageInfoFragment() + .WithHasPreviousPage() + .WithHasNextPage() + .WithStartCursor() + .WithEndCursor() + ); + +getSingleUseCodes.Fragment(beamConnectionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendGetSingleUseCodes(getSingleUseCodes); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdkBeam/BeamQueries.hpp" +#include "EnjinPlatformSdkBeam/Beam.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace enjin::platform::sdk::beam; +using namespace std; + +int main() { + + // Set up the query + GetSingleUseCodes getSingleUseCodes = GetSingleUseCodes() + .SetCode(make_shared("adab7ac8bda71c2e2bf8a3228f0a435f")); + + // Define Fragments for the returned data + BeamClaimFragment beamClaimFragment = BeamClaimFragment(); + BeamQrFragment beamQrFragment = BeamQrFragment().WithUrl(); + beamClaimFragment.WithQr(make_shared(beamQrFragment)); + + PageInfoFragment pageInfoFragment = PageInfoFragment() + .WithHasPreviousPage() + .WithHasNextPage() + .WithHasStartCursor() + .WithHasEndCursor(); + + // Connection fragments are used for paginated data + ConnectionFragment beamConnectionFragment = ConnectionFragment() + .WithEdges( + make_shared>( + EdgeFragment() + .WithCursor() + .WithNode(make_shared(beamClaimFragment)) + ) + ) + .WithTotalCount() + .WithPageInfo(make_shared(pageInfoFragment)); + + + getSingleUseCodes.SetFragment(make_shared>(beamConnectionFragment)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>>> futureResponse = SendGetSingleUseCodes(*client, getSingleUseCodes); + + // Get the platform response holding the HTTP data + PlatformResponsePtr>> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional>& singleUseCodes = gqlResult->GetData()->GetResult(); + + std::cout << singleUseCodes->GetTotalCount().value() << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} + +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + query GetSingleUseCodes + ( + $code: String! + ) { + GetSingleUseCodes(code: $code) { + edges { + cursor + node { + qr { + url + } + } + } + totalCount + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + `, + variables: { + code: "adab7ac8bda71c2e2bf8a3228f0a435f" //Set the beam code + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + query GetSingleUseCodes + ( + $code: String! + ) { + GetSingleUseCodes(code: $code) { + edges { + cursor + node { + qr { + url + } + } + } + totalCount + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + `, + variables: { + code: "adab7ac8bda71c2e2bf8a3228f0a435f" //Set the beam code + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +query GetSingleUseCodes +( + $code: String! +) { + GetSingleUseCodes(code: $code) { + edges { + cursor + node { + qr { + url + } + } + } + totalCount + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } +} +''' + +variables = { + 'code': "adab7ac8bda71c2e2bf8a3228f0a435f" #Set the beam code +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +:::tip This query might return items in different pages using Connections +To learn how to use GraphQL cursors for pagination, head to [Using the API --> Pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination). +::: + +The Enjin Beam is now ready to be shared to distribute tokens. Users can scan the QR code and instantly receive your token! + +:::info Explore More Arguments +For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. + +For instance, you'll find settings such as different Beam flags types with the `Flags` argument. +::: + +:::tip What's next? +Proceed to the [Freezing & Thawing](/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md) tutorial to learn more. +::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md new file mode 100644 index 0000000..677b54e --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md @@ -0,0 +1,1042 @@ +--- +title: "Freezing & Thawing" +slug: "freezing-thawing" +description: "Learn how to freeze and thaw tokens, controlling when your blockchain assets can be transferred or used, for added flexibility." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +"Freezing" refers to the process of temporarily suspending the transferability of a collection or a specific token. This action provides you greater control over the movement of assets, enhancing security and enabling unique use-cases. + +One such use-case is the implementation of "Soulbound" tokens. A Soulbound token is bound to a specific address and cannot be transferred out of the wallet it's minted on. This feature can be used to create unique gameplay mechanics, loyalty rewards, and more. + +:::note +Freezing only applies to ing and transfers, which also results in marketplace listings being unpurchasable. +Freezing does not suspend token minting. +::: + +### Freeze States + +A **freeze state** determines whether a token can be transferred and the conditions under which it remains locked or becomes transferrable. This feature enables token creators to define unique behaviors and restrictions for their assets, supporting scenarios like enhanced security, gameplay mechanics, or compliance needs. + +#### Explanation of Freeze States + +- **Permanent:** + The token is permanently frozen and cannot be transferred to another account under any circumstances. Use this state for tokens that are intended to stay bound to their original holder, such as "Soulbound" tokens for identity or loyalty purposes. + +- **Temporary:** + The token is temporarily frozen, restricting transfers until it is explicitly thawed by the collection owner. This state is ideal for implementing time-limited restrictions or conditional asset movement. + +- **Never:** + The token is always transferrable and cannot be frozen. Choose this state if you want the token to remain unrestricted in its movement across wallets and platforms. + +:::info What you'll need: +- Some [ Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for . +You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to freeze. +::: + +**There are two ways to Freeze / Thaw:** + +1. [Using the Enjin Dashboard](#option-a-using-the-enjin-dashboard) +2. [Using the GraphQL API & SDKs](#option-b-using-the-enjin-api--sdks) + +## Option A. Using the Enjin Dashboard + +:::tip Applying Freeze/Thaw Actions to Collections and Tokens +This tutorial illustrates the process of freezing a collection. +However, the same steps can be applied to freeze or thaw tokens. +Simply navigate to the corresponding menu for tokens instead of collections, or for thawing instead of freezing. +::: + +### Freezing an entire collection + +In the Platform menu, navigate to "**[Collections](https://platform.canary.enjin.io/collections)**". +**Locate the collection** you wish to freeze, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Freeze**" button. + +![Freezing a Collection](/img/guides/managing-tokens/freezing-collection.gif) + +Choose the freeze state, and click on the "**Freeze**" button. + +

+ Freeze Collection Form +

+ +The Transaction Request will then appear in the "**Transactions**" menu. + +

+ Freeze Collection Transaction Request Banner +

+ +![Pending Freeze Collection Transaction Request](/img/guides/managing-tokens/pending-freeze-collection-txn.png) + +Since this request requires a , it'll need to be signed with your Wallet. + +- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. +- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. + +## Option B. Using the Enjin API & SDKs + +### Freezing a Collection or Token + +By freezing a collection, all tokens within that collection will be frozen, meaning they cannot be burned or transferred out of the wallet they're currently in. + +### Freezing an entire collection + +Use the `Freeze` mutation and the `freezeType: COLLECTION` argument to freeze a collection. + + + +```graphql +mutation FreezeCollection{ + Freeze( + collectionId: 36105 #Specify the collection ID + freezeType: COLLECTION #For collection freezing + ){ + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation FreezeCollection($collection_id: BigInt!, $freeze_type: FreezeType!) {\r\n Freeze(collectionId: $collection_id, freezeType: $freeze_type) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"freeze_type":"COLLECTION"}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Setup the mutation +var freezeCollection = new Freeze() + .SetCollectionId(36105) + .SetFreezeType(FreezeType.Collection); + +// Define and assign the return data fragment to the mutation +var transactionFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +freezeCollection.Fragment(transactionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendFreeze(freezeCollection); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreMutations.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Set up the mutation + Freeze freezeCollection = Freeze() + .SetCollectionId(make_shared("36105")) + .SetFreezeType(FreezeType::Collection); + + // Define Fragment for the returned data + TransactionFragment transactionFragment = TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + + freezeCollection.SetFragment(make_shared(transactionFragment)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendFreeze(*client, freezeCollection); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& freezeCollectionResult = gqlResult->GetData()->GetResult(); + + std::cout << freezeCollectionResult->GetId().value() << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation FreezeCollection + ( + $collection_id: BigInt! + $freeze_type: FreezeType! + ) { + Freeze( + collectionId: $collection_id + freezeType: $freeze_type + ){ + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + freeze_type: "COLLECTION" //For collection freezing + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation FreezeCollection + ( + $collection_id: BigInt! + $freeze_type: FreezeType! + ) { + Freeze( + collectionId: $collection_id + freezeType: $freeze_type + ){ + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + freeze_type: "COLLECTION" //For collection freezing + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation FreezeCollection +( + $collection_id: BigInt! + $freeze_type: FreezeType! +) { + Freeze( + collectionId: $collection_id + freezeType: $freeze_type + ){ + id + method + state + } +} +''' + +variables = { + 'collection_id': 36105, #Specify the collection ID + 'freeze_type': "COLLECTION" #For collection freezing +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +Once the transaction is executed, all tokens within the specified collection will be frozen. + +### Freezing a single token + +Use the `Freeze` mutation and the `freezeType: TOKEN` argument to freeze a token. + + + +```graphql +mutation FreezeToken{ + Freeze( + collectionId: 36105 #Specify the collection ID + tokenId:{integer: 0} #Specify the token ID + freezeType: TOKEN #For token freezing + freezeState: TEMPORARY #Select the freeze state + ){ + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation FreezeToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $freeze_type: FreezeType!\r\n $freeze_state: FreezeStateType\r\n) {\r\n Freeze(\r\n collectionId: $collection_id\r\n tokenId: { integer: $token_id }\r\n freezeType: $freeze_type\r\n freezeState: $freeze_state\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":0,"freeze_type":"TOKEN","freeze_state":"TEMPORARY"}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Setup the mutation +var freezeToken = new Freeze() + .SetCollectionId(36105) + .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) + .SetFreezeType(FreezeType.Token) + .SetFreezeState(FreezeState.Temporary); + +// Define and assign the return data fragment to the mutation +var transactionFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +freezeToken.Fragment(transactionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendFreeze(freezeToken); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreMutations.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Setup an Encodable Token ID + EncodableTokenIdInput tokenId = EncodableTokenIdInput() + .SetInteger(make_shared("0")); + + // Set up the mutation + Freeze freezeToken = Freeze() + .SetCollectionId(make_shared("36105")) + .SetTokenId(make_shared(tokenId)) + .SetFreezeType(FreezeType::Token) + .SetFreezeState(FreezeState::Temporary); + + // Define Fragment for the returned data + TransactionFragment transactionFragment = TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + + freezeToken.SetFragment(make_shared(transactionFragment)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendFreeze(*client, freezeToken); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& freezeTokenResult = gqlResult->GetData()->GetResult(); + + std::cout << freezeTokenResult->GetId().value() << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation FreezeToken + ( + $collection_id: BigInt! + $token_id: BigInt! + $freeze_type: FreezeType! + $freeze_state: FreezeStateType + + ) { + Freeze( + collectionId: $collection_id + tokenId:{integer: $token_id} + freezeType: $freeze_type + freezeState: $freeze_state + ){ + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + token_id: 0, //Specify the token ID + freeze_type: "TOKEN", //For token freezing + freeze_state: "TEMPORARY" //Select the freeze state + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation FreezeToken + ( + $collection_id: BigInt! + $token_id: BigInt! + $freeze_type: FreezeType! + $freeze_state: FreezeStateType + + ) { + Freeze( + collectionId: $collection_id + tokenId:{integer: $token_id} + freezeType: $freeze_type + freezeState: $freeze_state + ){ + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + token_id: 0, //Specify the token ID + freeze_type: "TOKEN", //For token freezing + freeze_state: "TEMPORARY" //Select the freeze state + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation FreezeToken +( + $collection_id: BigInt! + $token_id: BigInt! + $freeze_type: FreezeType! + $freeze_state: FreezeStateType + +) { + Freeze( + collectionId: $collection_id + tokenId:{integer: $token_id} + freezeType: $freeze_type + freezeState: $freeze_state + ){ + id + method + state + } +} +''' + +variables = { + 'collection_id': 36105, #Specify the collection ID + 'token_id': 0, #Specify the token ID + 'freeze_type': "TOKEN", #For token freezing + 'freeze_state': "TEMPORARY" #Select the freeze state +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +Once the transaction is executed, the specified token will be frozen. + +### Thawing a Collection or Token + +Thawing a collection means allowing all tokens within that collection to be active again, so they can be transferred or burned as desired, removing the restrictions that kept them locked in a particular wallet. + +### Thawing an entire collection + +By thawing a collection, all tokens within that collection will be thawed, meaning they can be burned and transferred out of the wallet they're currently in. + +Use the `Thaw` mutation and the `freezeType: COLLECTION` argument to thaw a collection. + + + +```graphql +mutation ThawCollection{ + Thaw( + collectionId: 36105 #Specify the collection ID + freezeType: COLLECTION #For collection thawing + ){ + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation ThawCollection($collection_id: BigInt!, $freeze_type: FreezeType!) {\r\n Thaw(collectionId: $collection_id, freezeType: $freeze_type) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"freeze_type":"COLLECTION"}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Setup the mutation +var thawCollection = new Thaw() + .SetCollectionId(36105) + .SetFreezeType(FreezeType.Collection); + +// Define and assign the return data fragment to the mutation +var transactionFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +thawCollection.Fragment(transactionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendThaw(thawCollection); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreMutations.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Set up the mutation + Thaw thawCollection = Thaw() + .SetCollectionId(make_shared("36105")) + .SetFreezeType(FreezeType::Collection); + + // Define Fragment for the returned data + TransactionFragment transactionFragment = TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + + thawCollection.SetFragment(make_shared(transactionFragment)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendThaw(*client, thawCollection); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& thawCollectionResult = gqlResult->GetData()->GetResult(); + + std::cout << thawCollectionResult->GetId().value() << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation ThawCollection + ( + $collection_id: BigInt! + $freeze_type: FreezeType! + ) { + Thaw( + collectionId: $collection_id + freezeType: $freeze_type + ){ + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + freeze_type: "COLLECTION" //For collection thawing + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation ThawCollection + ( + $collection_id: BigInt! + $freeze_type: FreezeType! + ) { + Thaw( + collectionId: $collection_id + freezeType: $freeze_type + ){ + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + freeze_type: "COLLECTION" //For collection thawing + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation ThawCollection +( + $collection_id: BigInt! + $freeze_type: FreezeType! +) { + Thaw( + collectionId: $collection_id + freezeType: $freeze_type + ){ + id + method + state + } +} +''' + +variables = { + 'collection_id': 36105, #Specify the collection ID + 'freeze_type': "COLLECTION" #For collection thawing +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +Once the transaction is executed, all tokens within the specified collection will be thawed. + +### Thawing a single token + +Use the `Thaw` mutation and the `freezeType: TOKEN` argument to thaw a token. + + + +```graphql +mutation ThawToken{ + Thaw( + collectionId: 36105 #Specify the collection ID + tokenId:{integer: 0} #Specify the token ID + freezeType: TOKEN #For token thawing + ) { + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation ThawToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $freeze_type: FreezeType!\r\n) {\r\n Thaw(\r\n collectionId: $collection_id\r\n tokenId: { integer: $token_id }\r\n freezeType: $freeze_type\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":0,"freeze_type":"TOKEN"}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Setup the mutation +var thawToken = new Thaw() + .SetCollectionId(36105) + .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) + .SetFreezeType(FreezeType.Token); + +// Define and assign the return data fragment to the mutation +var transactionFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +thawToken.Fragment(transactionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendThaw(thawToken); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreMutations.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Setup an Encodable Token ID + EncodableTokenIdInput tokenId = EncodableTokenIdInput() + .SetInteger(make_shared("0")); + + // Set up the mutation + Thaw thawToken = Thaw() + .SetCollectionId(make_shared("36105")) + .SetTokenId(make_shared(tokenId)) + .SetFreezeType(FreezeType::Token); + + // Define Fragment for the returned data + TransactionFragment transactionFragment = TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + + thawToken.SetFragment(make_shared(transactionFragment)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendThaw(*client, thawToken); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& thawTokenResult = gqlResult->GetData()->GetResult(); + + std::cout << thawTokenResult->GetId().value() << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} + +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation ThawToken + ( + $collection_id: BigInt! + $token_id: BigInt! + $freeze_type: FreezeType! + ) { + Thaw( + collectionId: $collection_id + tokenId:{integer: $token_id} + freezeType: $freeze_type + ) { + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + token_id: 0, //Specify the token ID + freeze_type: "TOKEN" //For token thawing + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation ThawToken + ( + $collection_id: BigInt! + $token_id: BigInt! + $freeze_type: FreezeType! + ) { + Thaw( + collectionId: $collection_id + tokenId:{integer: $token_id} + freezeType: $freeze_type + ) { + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + token_id: 0, //Specify the token ID + freeze_type: "TOKEN" //For token thawing + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation ThawToken +( + $collection_id: BigInt! + $token_id: BigInt! + $freeze_type: FreezeType! +) { + Thaw( + collectionId: $collection_id + tokenId:{integer: $token_id} + freezeType: $freeze_type + ) { + id + method + state + } +} +''' + +variables = { + 'collection_id': 36105, #Specify the collection ID + 'token_id': 0, #Specify the token ID + 'freeze_type': "TOKEN" #For token thawing +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +Once the transaction is executed, the specified token will be thawed. + +:::info Explore More Arguments +For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. + +For instance, you'll find settings such as `continueOnFailure` to skip data that would cause the whole batch to fail, or the ability to sign using a managed wallet with the `signingAccount` argument. +::: + +:::tip Need to send a transaction request to user's wallet? +This can be done using Enjin Platform API & WalletConnect! +To learn more, check out the [Using WalletConnect page](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md). +::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md new file mode 100644 index 0000000..55cea11 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md @@ -0,0 +1,640 @@ +--- +title: "Melting / Destroying Tokens and Collections" +slug: "burning-destroying-tokens" +description: "Understand the process of burning or destroying tokens in the Enjin ecosystem, allowing you to permanently remove assets from circulation." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +"Melting" (often called "Burning") refers to the process of decreasing a token's supply and removing it from circulation, or in some cases, even removing the token from the blockchain entirely. +Melting a token with releases the Infused ENJ to the holder. + +:::info What you'll need: +- Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for . +You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) or a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to melt/destroy. +::: + +**There are two ways to Melt a token:** + +1. [Using the Platform User Interface](#option-a-using-the-enjin-dashboard) +2. [Using the GraphQL API](#option-b-using-the-enjin-api--sdks) + +## Option A. Using the Enjin Dashboard + +### Melting token's supply + +In the Platform menu, navigate to "**[Tokens](https://platform.canary.enjin.io/tokens)**". +**Locate the token** you wish to melt, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Burn**" button. + +![melting a Token](/img/guides/managing-tokens/burning-token.gif) + +Insert the amount of tokens to melt, and click on the "**Burn**" button. + +

+ Melt Token Form +

+ +The Transaction Request will then appear in the "**Transactions**" menu. + +

+ Melt Transaction Request Banner +

+ +![Pending melt Transaction](/img/guides/managing-tokens/pending-burn-token-txn.png) + +Since this request requires a , it'll need to be signed with your Wallet. + +- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. +- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. + +### Destroying a token and removing it from the Blockchain + +:::info To destroy a token, these requirements must be met: +- The caller is the collection owner +- The token has no attributes + - If the token has attributes, you can remove the attributes by clicking the **3 vertical dots** (**⋮**) next to the token, followed by "**Attributes**" and selecting "**Remove All"**. +- The token has 0 supply + - If the token has supply, you can follow the above guide [Burning token's supply](#melting-tokens-supply) to remove all token supply (as long as you own all of the token's supply) + Note - you can remove the supply and destroy the token in the same melt transaction. +::: + +Melting a token and destroying it are two different actions. +The action demonstrated above is the action of melting a token, which decreases it's circulating supply. +While destroying a token removes the token from the blockchain, and retrieves the to the collection owner. + +To destroy a token, follow the above instructions for Melting a token, but make sure to tick the `Remove Token Storage` box. + +

+ Destroying a Token +

+ +### Destroying a collection + +:::info To destroy a collection, these requirements must be met: +- The caller is the collection owner +- The collection has no attributes + - If the collection has attributes, you can remove the attributes by clicking the **3 vertical dots** (**⋮**) next to the collection, followed by "**Attributes**" and selecting "**Remove All"**. +- The collection has 0 tokens **in storage** + - If the collection has some tokens, you can follow the above guide [Destroying a token and removing it from the Blockchain](#destroying-a-token-and-removing-it-from-the-blockchain) for each of the tokens in the collection, to destroy them all. +::: + +In the Platform menu, navigate to "**[Collections](https://platform.canary.enjin.io/collections)**". +**Locate the collection** you wish to destroy, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Destroy**" button. + +![Destroying a Collection](/img/guides/managing-tokens/destroying-collection.gif) + +Then, confirm by clicking the "**Destroy**" button + +

+ Destroy Collection Form +

+ +The Transaction Request will then appear in the "**Transactions**" menu + +

+ Destroy Collection Transaction Request Banner +

+ +![Pending Destroy Collection Transaction](/img/guides/managing-tokens/pending-destroy-collection-txn.png) + +Since this request requires a , it'll need to be signed with your Wallet. + +- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. +- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. + +## Option B. Using the Enjin API & SDKs + +### Melting token's supply + +Use the `Burn` mutation: + + + +```graphql +mutation BurnToken{ + Burn( + collectionId: 68844 #Specify the Collection ID + params: { + tokenId: {integer: 0} #Specify the Token ID + amount: 1 #Specify the amount of supply to burn + } + ) { + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation BurnToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $amount: BigInt!\r\n) {\r\n Burn(\r\n collectionId: $collection_id\r\n params: { tokenId: { integer: $token_id }, amount: $amount }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":5,"amount":1}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Set up the burn params +var burnParams = new BurnParamsInput() + .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) // Set the token id. + .SetAmount(1); // Set the amount to burn. + +// Set up the mutation +var burn = new Burn() + .SetCollectionId(68844) // Set the collection id. + .SetParams(burnParams); // Set the burn params. + +// Define and assign the return data fragment to the mutation +var burnFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +burn.Fragment(burnFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendBurn(burn); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +Snippet In Progress +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation BurnToken( + $collection_id: BigInt! + $token_id: BigInt! + $amount: BigInt! + ){ + Burn( + collectionId: $collection_id + params: { + tokenId: {integer: $token_id} + amount: $amount + } + ) { + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + token_id: 5, //Specify the amount of supply to burn + amount: 1 //Specify the amount of supply to burn + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation BurnToken( + $collection_id: BigInt! + $token_id: BigInt! + $amount: BigInt! + ){ + Burn( + collectionId: $collection_id + params: { + tokenId: {integer: $token_id} + amount: $amount + } + ) { + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + token_id: 5, //Specify the amount of supply to burn + amount: 1 //Specify the amount of supply to burn + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation BurnToken( + $collection_id: BigInt! + $token_id: BigInt! + $amount: BigInt! +){ + Burn( + collectionId: $collection_id + params: { + tokenId: {integer: $token_id} + amount: $amount + } + ) { + id + method + state + } +} +''' + +variables = { + 'collection_id': 36105, #Specify the collection ID + 'token_id': 5, #Specify the amount of supply to burn + 'amount': 1 #Specify the amount of supply to burn +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +Once the transaction is executed, the token supply will be burned + +### Destroying a token and removing it from the Blockchain + +:::info To destroy a token, these requirements must be met: +- The caller is the collection owner +- The token has no attributes + - If the token has attributes, they can be removed using the `RemoveAllAttributes` mutation +- The token has 0 supply + - You can remove the supply and destroy the token in the same Burn transaction. +::: + +Use the `Burn` mutation, and add `removeTokenStorage: true` property + + + +```graphql +mutation DestroyToken{ + Burn( + collectionId: 68844 #Specify the Collection ID + params: { + tokenId: {integer: 0} #Specify the Token ID + amount: 1 #Specify the amount of supply to burn + removeTokenStorage: true + } + ) { + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation BurnToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $amount: BigInt!\r\n) {\r\n Burn(\r\n collectionId: $collection_id\r\n params: {\r\n tokenId: { integer: $token_id }\r\n amount: $amount\r\n removeTokenStorage: true\r\n }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":5,"amount":1}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Set up the burn params +var burnParams = new BurnParamsInput() + .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) // Set the token id. + .SetAmount(1) // Set the amount to burn. + .SetRemoveTokenStorage(true); // Set whether the token storage will be removed if no tokens are left. + +// Set up the mutation +var burn = new Burn() + .SetCollectionId(68844) // Set the collection id. + .SetParams(burnParams); // Set the burn params. + +// Define and assign the return data fragment to the mutation +var burnFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +burn.Fragment(burnFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendBurn(burn); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +Snippet In Progress +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation BurnToken( + $collection_id: BigInt! + $token_id: BigInt! + $amount: BigInt! + ){ + Burn( + collectionId: $collection_id + params: { + tokenId: {integer: $token_id} + amount: $amount + removeTokenStorage: true + } + ) { + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + token_id: 5, //Specify the amount of supply to burn + amount: 1 //Specify the amount of supply to burn + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation BurnToken( + $collection_id: BigInt! + $token_id: BigInt! + $amount: BigInt! + ){ + Burn( + collectionId: $collection_id + params: { + tokenId: {integer: $token_id} + amount: $amount + removeTokenStorage: true + } + ) { + id + method + state + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + token_id: 5, //Specify the amount of supply to burn + amount: 1 //Specify the amount of supply to burn + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation BurnToken( + $collection_id: BigInt! + $token_id: BigInt! + $amount: BigInt! +){ + Burn( + collectionId: $collection_id + params: { + tokenId: {integer: $token_id} + amount: $amount + removeTokenStorage: true + } + ) { + id + method + state + } +} +''' + +variables = { + 'collection_id': 36105, #Specify the collection ID + 'token_id': 5, #Specify the amount of supply to burn + 'amount': 1 #Specify the amount of supply to burn +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +Once the transaction is executed, the token will be destroyed and the will be retrieved. + +### Destroying a collection + +:::info To destroy a collection, these requirements must be met: +- The caller is the collection owner +- The collection has no attributes + - If the collection has attributes, they can be removed using the `RemoveAllAttributes` mutation +- The collection has 0 tokens **in storage** + - If the collection has some tokens, you can the above instructions for [Destroying a token](#destroying-a-token-and-removing-it-from-the-blockchain-1) for each of the tokens in the collection, to destroy them all. +::: + + + +```graphql +mutation DestroyCollection { + DestroyCollection( + collectionId: 68844 #Specify the Collection ID + ) { + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation DestroyCollection($collection_id: BigInt!) {\r\n DestroyCollection(collectionId: $collection_id) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Set up the mutation +var destroyCollection = new DestroyCollection() + .SetCollectionId(68844); // Set the collection id. + +// Define and assign the return data fragment to the mutation +var destrotCollectionFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +destroyCollection.Fragment(destrotCollectionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendDestroyCollection(destroyCollection); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +Snippet In Progress +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation DestroyCollection($collection_id: BigInt!){ + DestroyCollection( + collectionId: $collection_id + ) { + id + method + state + } + } + `, + variables: { + collection_id: 36105 //Specify the collection ID + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation DestroyCollection($collection_id: BigInt!){ + DestroyCollection( + collectionId: $collection_id + ) { + id + method + state + } + } + `, + variables: { + collection_id: 36105 //Specify the collection ID + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation DestroyCollection($collection_id: BigInt!){ + DestroyCollection( + collectionId: $collection_id + ) { + id + method + state + } +} +''' + +variables = { + 'collection_id': 36105 #Specify the collection ID +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +Once the transaction is executed, the collection will be destroyed. \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md new file mode 100644 index 0000000..13a5607 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md @@ -0,0 +1,670 @@ +--- +title: "Fetching Token Holders" +slug: "fetching-token-holders" +description: "Fetch token and collection holders using the Enjin Platform API." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +This guide explains how to fetch the current holders of a specific token or the owners of a collection using the Enjin Platform API. + +Accessing this data is valuable for several purposes: +- **Identify Token Holders:** Get a list of addresses that hold your tokens or own items from your collection. +- **Targeted Actions:** Use the list to perform actions specifically for your holders or owners, such as distributing rewards or communicating directly. +- **Analyze Distribution:** Understand how your tokens or collection items are distributed among users. + +:::info What you'll need: +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +::: + +## Fetching Token Holders with the [Enjin API](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) + +:::warning Fetching Tokens Limitations +Please note that the `GetToken` query is limited to tokens from collections that were created via the auth-ed Enjin Platform account. +To get a token that was created elsewhere (via a different Enjin Platform account / [NFT.io](https://nft.io) / [Enjin Console](https://console.enjin.io) / etc.) the collection must be "Tracked" first, or the query response will return an error. +Learn more about tracking a collection in the [Tracking Collections section below](#tracking-collections). +::: + +To get holders of a specific token, we use the `GetToken` query and include the the `accounts` in the query response: + +**Query:** + + + +```graphql +query FetchingTokenHolders{ + GetToken( + collectionId: 36105 #Specify the collection ID + tokenId: {integer: 0} #Specify the token ID + ){ + accounts{ + edges{ + node{ + balance + wallet{ + account{ + address + } + } + } + } + } + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"query FetchingTokenHolders($collection_id: BigInt!, $token_id: BigInt!) {\r\n GetToken(collectionId: $collection_id, tokenId: { integer: $token_id }) {\r\n accounts {\r\n edges {\r\n node {\r\n balance\r\n wallet {\r\n account {\r\n address\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}","variables":{"collection_id":36105,"token_id":0}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Setup the query +var getToken = new GetToken() + .SetCollectionId(36105) + .SetTokenId(new EncodableTokenIdInput().SetInteger(0)); + +// Define and assign the return data fragment to the query +var tokenFragment = new TokenFragment() + .WithAccounts(new ConnectionFragment() + .WithEdges(new EdgeFragment() + .WithNode(new TokenAccountFragment() + .WithBalance() + .WithWallet(new WalletFragment() + .WithAccount(new AccountFragment() + .WithAddress() + ) + ) + ) + ) + ); + +getToken.Fragment(tokenFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendGetToken(getToken); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreQueries.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Setup an Encodable Token ID + EncodableTokenIdInput tokenId = EncodableTokenIdInput() + .SetInteger(make_shared("0")); + + // Set up the query + GetToken getToken = GetToken() + .SetCollectionId(make_shared("36105")) + .SetTokenId(make_shared(tokenId)); + + // Define and assign the return data fragments to the query + AccountFragment accountFragment = AccountFragment() + .WithAddress(); + WalletFragment walletFragment = WalletFragment() + .WithAccount(make_shared(accountFragment)); + TokenAccountFragment tokenAccountFragment = TokenAccountFragment() + .WithBalance() + .WithWallet(make_shared(walletFragment)); + EdgeFragment edgeFragment = EdgeFragment() + .WithNode(make_shared(tokenAccountFragment)); + ConnectionFragment connectionFragment = ConnectionFragment() + .WithEdges(make_shared>(edgeFragment)); + + TokenFragment tokenFragment = TokenFragment() + .WithAccounts(make_shared>(connectionFragment)); + + getToken.SetFragment(make_shared(tokenFragment)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendGetToken(*client, getToken); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& getTokenResult = gqlResult->GetData()->GetResult(); + + std::cout << getTokenResult->GetMinimumBalance().value() << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + query FetchingTokenHolders + ( + $collection_id: BigInt! + $token_id: BigInt! + ) { + GetToken( + collectionId: $collection_id + tokenId: {integer: $token_id} + ){ + accounts{ + edges{ + node{ + balance + wallet{ + account{ + address + } + } + } + } + } + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + token_id: 0 //Specify the token ID + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + query FetchingTokenHolders + ( + $collection_id: BigInt! + $token_id: BigInt! + ) { + GetToken( + collectionId: $collection_id + tokenId: {integer: $token_id} + ){ + accounts{ + edges{ + node{ + balance + wallet{ + account{ + address + } + } + } + } + } + } + } + `, + variables: { + collection_id: 36105, //Specify the collection ID + token_id: 0 //Specify the token ID + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +query FetchingTokenHolders +( + $collection_id: BigInt! + $token_id: BigInt! +) { + GetToken( + collectionId: $collection_id + tokenId: {integer: $token_id} + ){ + accounts{ + edges{ + node{ + balance + wallet{ + account{ + address + } + } + } + } + } + } +} +''' + +variables = { + 'collection_id': 36105, #Specify the collection ID + 'token_id': 0 #Specify the token ID +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +**Response:** + +```json +{ + "data": { + "GetToken": { + "accounts": { + "edges": [ + { + "node": { + "balance": "4", + "wallet": { + "account": { + "address": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" + } + } + } + }, + { + "node": { + "balance": "2", + "wallet": { + "account": { + "address": "cxJXQKFB4ZWJzutwEtZvTQ9KLucdsaWgzyef5pv9R7x98Dmwp" + } + } + } + } + ] + } + } + } +} +``` + +:::tip Using Pagination +The response may be displayed on several pages. To view all of it, you may need to follow steps for [pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination) which allows you to flip through the pages. +::: + +## Fetching Collection Holders with the [Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) + +:::warning Fetching Collections Limitations +Please note that the `GetCollections` query is limited to collections that were created via the auth-ed Enjin Platform Cloud account. +To get a collection that was created elsewhere (via a different Enjin Platform Cloud account / [NFT.io](https://nft.io) / [Enjin Console](https://console.enjin.io) / etc.) the collection must be "Tracked" first, or the query response will return an error. +Learn more about tracking a collection in the [Tracking Collections section below](#tracking-collections). +::: + +For an [NFT collection](/02-guides/01-platform/03-advanced-mechanics/03-enforced-rarity.md#nft-collection), we can use the `GetCollection` query and include the the `accounts` in the query response to find out how many NFTs from the collection are owned by each address. + +**Query:** + + + +```graphql +query FetchingCollectionHolders{ + GetCollection(collectionId: 36105){ #Specify the collection ID + accounts{ + edges{ + node{ + accountCount + wallet{ + account{ + address + } + } + } + } + } + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"query FetchingCollectionHolders($collection_id: BigInt!) {\r\n GetCollection(collectionId: $collection_id) {\r\n accounts {\r\n edges {\r\n node {\r\n accountCount\r\n wallet {\r\n account {\r\n address\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}","variables":{"collection_id":36105}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Setup the query +var getCollection = new GetCollection() + .SetCollectionId(36105); + +// Define and assign the return data fragment to the query +var collectionFragment = new CollectionFragment() + .WithAccounts(new ConnectionFragment() + .WithEdges(new EdgeFragment() + .WithNode(new CollectionAccountFragment() + .WithAccountCount() + .WithWallet(new WalletFragment() + .WithAccount(new AccountFragment() + .WithAddress() + ) + ) + ) + ) + ); + +getCollection.Fragment(collectionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendGetCollection(getCollection); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); CC +``` + + +```cpp +#include "EnjinPlatformSdk/CoreQueries.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Set up the query + GetCollection getCollection = GetCollection() + .SetCollectionId(make_shared("36105")); + + // Define and assign the return data fragments to the query + CollectionAccountFragment collectionAccountFragment = CollectionAccountFragment() + .WithAccountCount() + .WithWallet(make_shared(WalletFragment() + .WithAccount(make_shared(AccountFragment() + .WithAddress() + ) + ) + ) + ); + + EdgeFragment edgeFragment = EdgeFragment() + .WithNode(make_shared(collectionAccountFragment)); + + ConnectionFragment connectionFragment = ConnectionFragment() + .WithEdges(make_shared>(edgeFragment)); + + CollectionFragment collectionFragment = CollectionFragment() + .WithAccounts(make_shared>(connectionFragment)); + + + getCollection.SetFragment(make_shared(collectionFragment)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendGetCollection(*client, getCollection); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& getCollectionResult = gqlResult->GetData()->GetResult(); + + std::cout << getCollectionResult->GetCollectionId().value() << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + query FetchingCollectionHolders + ( + $collection_id: BigInt! + ) { + GetCollection(collectionId: $collection_id){ + accounts{ + edges{ + node{ + accountCount + wallet{ + account{ + address + } + } + } + } + } + } + } + `, + variables: { + collection_id: 36105 //Specify the collection ID + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + query FetchingCollectionHolders + ( + $collection_id: BigInt! + ) { + GetCollection(collectionId: $collection_id){ + accounts{ + edges{ + node{ + accountCount + wallet{ + account{ + address + } + } + } + } + } + } + } + `, + variables: { + collection_id: 36105 //Specify the collection ID + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +query FetchingCollectionHolders +( + $collection_id: BigInt! +) { + GetCollection(collectionId: $collection_id){ + accounts{ + edges{ + node{ + accountCount + wallet{ + account{ + address + } + } + } + } + } + } +} +''' + +variables = { + 'collection_id': 36105 #Specify the collection ID +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +**Response:** + +```json +{ + "data": { + "GetCollection": { + "accounts": { + "edges": [ + { + "node": { + "accountCount": 3, + "wallet": { + "account": { + "address": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" + } + } + } + }, + { + "node": { + "accountCount": 2, + "wallet": { + "account": { + "address": "cxJXQKFB4ZWJzutwEtZvTQ9KLucdsaWgzyef5pv9R7x98Dmwp" + } + } + } + } + ] + } + } + } +} +``` + +:::tip Using Pagination +The response may be displayed on several pages. To view all of it, you may need to follow steps for [pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination) which allows you to flip through the pages. +::: + +:::info Explore More Arguments +For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. +::: + +## Tracking Collections + +The `GetToken`, `GetTokens`, `GetCollection`, and `GetCollections` queries are limited only to collections and tokens that were created via the auth-ed Enjin Platform account. To use these queries with collections / tokens created externally (via a different Enjin Platform account / [NFT.io](https://nft.io) / [Enjin Console](https://console.enjin.io) / etc.) the collection must be "Tracked" first, or the query response will return an error. + +To track a collection, head over to the collections page on Enjin Platform: https://platform.enjin.io/collections, click on the "Track" button, insert the collection ID of the collection you want to track and click "Track". + +![Track Collection](/img/guides/managing-tokens/track-collection.png) + +:::tip Need to track collections programmatically? +You can use the `AddToTracked` mutation to track a collection programmatically. +::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/_category_.json new file mode 100644 index 0000000..7536ec8 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Managing Tokens" +} + \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md new file mode 100644 index 0000000..f5e1df5 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md @@ -0,0 +1,237 @@ +--- +title: "Using WalletConnect" +slug: "using-wallet-connect" +description: "Find out how to use WalletConnect to seamlessly connect user wallets to your application, enhancing blockchain integration and accessibility." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +[WalletConnect](https://docs.walletconnect.com/) is an open-source protocol that connects your mobile wallet to decentralized applications. +Using the Platform API and WalletConnect you can send any transaction request directly to your user's wallet app. + +:::danger **Maintenance Notice** +The `polkadot-onboard` sample repository used in this guide is **no longer maintained** and may not function as expected. + +**Note:** WalletConnect itself remains fully functional and compatible with Enjin Blockchain. This guide is retained as a reference for the implementation logic for developers building their own integrations. + +**Upcoming Feature:** A native solution is in development to enable direct transaction requests from the Enjin Platform to the Enjin Wallet, removing the need for third-party services. +::: + +:::info What you'll need: +- Node.js 18+ and yarn installed +- A mobile device with [Enjin Wallet app](https://enjin.io/wallet) installed +- A Canary wallet with at least 6.5 ENJ in it to cover for and . +You can obtain cENJ for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md) (Canary). +::: + +## Install and configure the sample project + +On production apps, you will need [WalletConnect's SDK](https://docs.walletconnect.com/web3wallet/about), but for this guide we'll be using the (now deprecated) sample project called [polkadot-onboard](https://github.com/enjin-forks/polkadot-onboard.git). + +Start by cloning the polkadot-onboard repository: https://github.com/enjin-forks/polkadot-onboard.git + +Once cloned, install the packages, install the workspace, build the workspace, and start the example project. +To do that, navigate to the cloned repository folder and run the following commands: + +:::info **Note:** +The commands below are Bash commands, which are commonly run in a Unix-like environment (such as macOS, or Linux). +Windows users may encounter errors if they try running these commands directly in Command Prompt or PowerShell. +To run them successfully on Windows, use a Bash-compatible terminal, like [Git Bash](https://gitforwindows.org/). +::: + +```bash +yarn +yarn install:workspace +yarn build:workspace +``` + +Now, let's run the project and test it. You can do that by running the following command inside the root folder of the project: + +```bash +yarn start:react-next +``` + +Navigate to http://localhost:3000 using your browser. If everything went well, a sample page should load up. + +:::info Configured Chain +The sample project is set to broadcast transaction on the Canary Matrixchain Testnet. +To configure a different blockchain, you can adjust the `chainIds` in the `examples/react-next/components/ConnectContainer.tsx` file. + +Chain IDs on Enjin Chains: +- Mainnet Matrixchain: `polkadot:3af4ff48ec76d2efc8476730f423ac07` +- Mainnet Relaychain: `polkadot:d8761d3c88f26dc12875c00d3165f7d6` +- Canary Matrixchain: `polkadot:a37725fd8943d2a524cb7ecc65da438f` +- Canary Relaychain: `polkadot:735d8773c63e74ff8490fee5751ac07e` +::: + +## Connect user's wallet + +To send transactions to your user, he must be connected to your dapp. +Clicking `Get Wallets`, followed by `Polkadot Demo 2.0`, initiates a connection between your user and your dapp. + +

+ Connect Wallet Modal +

+ +Once the user scans the QR code with his Enjin wallet app (or any other wallet with WalletConnect & Enjin Blockchain support) he will be asked to approve the connection: + +

+ Approve Connection Request +

+ +Once a connection was made, you'll receive a list of all addresses connected from your user's wallet. +Now you are able to send any transaction request to your user's wallet. +To demonstrate a sample transaction request, click the "**Submit Transaction with Polkadot.JS**" button on one of the displayed addresses. + +

+ Connected Wallets +

+ +A Transaction Request dialog will pop up on user's connected wallet app, asking for approval. + +

+ Confirm Transaction Request +

+ +Once the transaction request is confirmed by the user, his wallet will sign and broadcast a message saying "I am signing this transaction!". + +In the next section, you'll learn how to construct your own transaction requests using Enjin Platform API + +## How to construct a custom transaction? + +In the example above, we have sent a request to broadcast a transaction that was already constructed for us. +But how was that transaction constructed and how can we send any transaction we want? Let's find out! + +At `examples/react-next/pages/api`, there's two endpoints we'll be using: + +- `transaction.tsx` endpoint which constructs the transaction +- `send.tsx` which broadcasts the transaction request to the blockchain using Enjin Platform API. + +First, we need to insert our Enjin platform API key to be able to interact with it. +If you haven't created an api key yet, you can do so in https://platform.canary.enjin.io/settings. + +In both `examples/react-next/pages/api/transaction.tsx` and `examples/react-next/pages/api/send.tsx` files, replace `your-api-key` with your api key (note, there's three different api key fields that needs to be updated). + +Once we've configured the Enjin Platform API key, rebuild and restart the project: + +:::info **Note:** +The commands below are Bash commands, which are commonly run in a Unix-like environment (such as macOS, or Linux). +Windows users may encounter errors if they try running these commands directly in Command Prompt or PowerShell. +To run them successfully on Windows, use a Bash-compatible terminal, like [Git Bash](https://gitforwindows.org/). +::: + +```bash +yarn build:workspace +yarn start:react-next +``` + +Now that the project is authenticated with the Enjin Platform, we can go ahead and press the "**Submit Transaction with Platform**" button. +A `CreateCollection` request will be sent to your wallet, and once confirmed, the transaction will be signed and broadcasted to the blockchain. +Now it's time to break it down and understand how it works: + +### Step #1: Constructing the transaction call + +We are constructing the transaction using the `transaction.tsx` endpoint. +First, we make a query to the user's address to get its nonce: + +```javascript + const walletResponse = await fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + mode: 'no-cors', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'my-auth-key', + }, + body: JSON.stringify({ + query: `query GetWallet($account: String!) { + GetWallet(account: $account) { + nonce + } + }`, + variables: { + account: req.query.address + } + }) + }); +``` + +Once we get the nonce, we prepare the `CreateCollection` transaction call using Enjin Platform API. +You can use any other mutation Enjin Platform API offers, but for this example we're using CreateCollection. + +```javascript + const response = await fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + mode: 'no-cors', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'my-auth-key', + }, + body: JSON.stringify({ + query: `mutation CreateCollection($nonce: Int!, $signingAccount: String!) { + CreateCollection( + mintPolicy: { forceSingleMint: false }, + signingAccount: $signingAccount + ) { + id + signingPayloadJson(nonce: $nonce) + } + }`, + variables: { + signingAccount: req.query.address, + nonce: nonce + } + }) + }); +``` + +We are also passing the `signingAccount`. This is necessary so your daemon doesn't sign the transaction. +In the mutation response, we're asking for the `id` and `signingPayloadJson`, which we will use in the next step. + +### Step #2: Sending the Transaction Request to user's wallet + +With the payload in hand we pass that to our WalletConnect signer and ask it to sign. In the example project, this is done at: `examples/react-next/components/AccountBox.tsx` + +```javascript +const txId = data?.data?.CreateCollection?.id; +const payload = data?.data?.CreateCollection?.signingPayloadJson; +const { signature } = await signer?.signPayload(payload) +``` + +The above will trigger the Transaction Request dialog you saw previously, asking for the user to confirm signing the transaction. + +### Step #3: Broadcasting the signed transaction + +Finally, we received the `txId`, `signature`, and `payload` from the user's wallet. +We can now broadcast the signed transaction to the blockchain using the Enjin Platform API `SendTransaction` mutation. +This is done at: `examples/react-next/pages/api/send.tsx` + +```javascript + const response = await fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + mode: 'no-cors', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'my-auth-key', + }, + body: JSON.stringify({ + query: `mutation SendTransaction($id: Int!, $signingPayloadJson: Object!, $signature: String!) { + SendTransaction(id: $id, signingPayloadJson: $signingPayloadJson, signature: $signature) + }`, + variables: { + id: Number.parseInt(req.query.id), + signingPayloadJson: JSON.parse(req.query.signingPayloadJson), + signature: req.query.signature + } + }) + }); +``` + +The Enjin Platform will broadcast the transaction to the blockchain and keep track of it. +You can see the transaction status in Enjin Platform: https://platform.canary.enjin.io/transactions + +*** + +**Congratulations!** you can now send any transaction request to your user's wallets. diff --git a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md new file mode 100644 index 0000000..4fc55c4 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md @@ -0,0 +1,717 @@ +--- +title: "Verifying Wallets" +slug: "verifying-wallets" +description: "Learn how to verify user wallets to ensure secure and trustworthy transactions within the Enjin platform, enhancing security and reliability." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Before you can interact with a user's blockchain items or associate their in-app identity with their blockchain address, you need a reliable way to confirm they own the wallet they provide. The Enjin Platform's wallet verification process provides a secure method to achieve this. + +By verifying a user's wallet, you gain certainty that the public wallet address shared genuinely belongs to them. This verified link is essential for securely accessing their blockchain inventory, delivering items, or granting ownership-based perks within your application. + +The verification process typically involves: + +1. Your application requests a unique verification code and QR code from the Enjin API. +2. The user scans the QR code with their . +3. The confirms ownership and securely provides the wallet address to the Enjin Platform. +4. Your application retrieves the verified address from the Platform. + +Once verified, you can confidently associate this blockchain address with the user's account in your system, enabling seamless blockchain interactions. + +:::tip +If you need to send requests to user wallets, use [WalletConnect](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md) instead. +::: + +:::info What you'll need: +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +::: + +## Verify Wallet with the [Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) + +### Step 1. Initiate an Account Request + +To initiate the account request process, run the following query: + +**Query:** + + + +```graphql +query RequestAccount{ + RequestAccount{ + qrCode #Returns a QR code for your user to scan + verificationId #Save this for next step + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"query RequestAccount {\r\n RequestAccount {\r\n qrCode #Returns a QR code for your user to scan\r\n verificationId #Save this for next step\r\n }\r\n}\r\n","variables":{}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Setup the query +var requestAccount = new RequestAccount(); + +// Define and assign the return data fragment to the query +var accountRequestFragment = new AccountRequestFragment() + .WithQrCode() + .WithVerificationId(); + +requestAccount.Fragment(accountRequestFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendRequestAccount(requestAccount); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreQueries.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Set up the query + RequestAccount requestAccount = RequestAccount(); + + // Define and assign the return data fragments to the query + AccountRequestFragment accountRequestFragment = AccountRequestFragment() + .WithQrCode() + .WithVerificationId(); + + requestAccount.SetFragment(make_shared(accountRequestFragment)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendRequestAccount(*client, requestAccount); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& getAccountRequestResult = gqlResult->GetData()->GetResult(); + + std::cout << getAccountRequestResult->GetVerificationId().value() << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + query RequestAccount{ + RequestAccount{ + qrCode #Returns a QR code for your user to scan + verificationId #Save this for next step + } + } + ` + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + query RequestAccount{ + RequestAccount{ + qrCode #Returns a QR code for your user to scan + verificationId #Save this for next step + } + } + ` +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = `''' +query RequestAccount{ + RequestAccount{ + qrCode #Returns a QR code for your user to scan + verificationId #Save this for next step + } +} +'''` + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +**Response:** + +```json +{ + "data": { + "RequestAccount": { + "qrCode": "https://platform.canary.enjin.io/qr?s=512&f=png&d=https://platform.canary.enjin.io/proof/MHg3OGFmYWIxYWQxMjk0YzFlZWY4MjdlNmFjNTM1MmJiOTVmNmFmNWJhNmUyNjk5ZGRkOTI3YjAwNmQ3ZDk0MzZjO2Vwc3I6QTIwMTBBSzlM", + "verificationId": "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" + } + } +} +``` + +This will return a link to a QR Code for you to display to your user, and a unique verification id to track the request. +In your application, you'll display this QR code to your user and instruct them to scan it with their . + +### Step 2: Verify Account + +To confirm that the user scanned the QR code and approved the request, you can retrieve the account using two methods. + +#### Option A: `GetWallet` + +Using `GetWallet` and passing the `verificationId` from step 1, you will be able to get any information related to his wallet if the verification process succeeded. + +**Query:** + + + +```graphql +query GetVerifiedWallet{ + GetWallet(verificationId: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"){ #Set the verificationId from the RequestAccount response + account{ + address + } + balances{ + free + } + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"query GetVerifiedWallet($verification_id: String!) {\r\n GetWallet(verificationId: $verification_id) {\r\n account {\r\n address\r\n }\r\n balances {\r\n free\r\n }\r\n }\r\n}","variables":{"verification_id":"0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Setup the query +var getWallet = new GetWallet() + .SetVerificationId("0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"); + +// Define and assign the return data fragment to the query +var walletFragment = new WalletFragment() + .WithAccount(new AccountFragment() + .WithAddress() + ) + .WithBalances(new BalancesFragment() + .WithFree() + ); + +getWallet.Fragment(walletFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendGetWallet(getWallet); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); + +``` + + +```cpp +#include "EnjinPlatformSdk/CoreQueries.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Set up the query + GetWallet getWallet = GetWallet() + .SetVerificationId(make_shared("0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c")); + + // Define and assign the return data fragments to the query + AccountFragment accountFragment = AccountFragment() + .WithAddress(); + BalancesFragment balancesFragment = BalancesFragment() + .WithFree(); + WalletFragment walletFragment = WalletFragment() + .WithAccount(make_shared(accountFragment)) + .WithBalances(make_shared(balancesFragment)); + + getWallet.SetFragment(make_shared(walletFragment)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendGetWallet(*client, getWallet); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& getWalletResult = gqlResult->GetData()->GetResult(); + + std::cout << getWallet.GetName() << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} + +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + query GetVerifiedWallet($verification_id: String!) { + GetWallet(verificationId: $verification_id) { + account{ + address + } + balances{ + free + } + } + } + `, + variables: { + verification_id: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" //Set the verificationId from the RequestAccount response + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + query GetVerifiedWallet($verification_id: String!) { + GetWallet(verificationId: $verification_id) { + account{ + address + } + balances{ + free + } + } + } + `, + variables: { + verification_id: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" //Set the verificationId from the RequestAccount response + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +query GetVerifiedWallet($verification_id: String!) { + GetWallet(verificationId: $verification_id) { + account{ + address + } + balances{ + free + } + } +} +''' + +variables = { + 'verification_id': "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" #Set the verificationId from the RequestAccount response +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +**Response: If user has verified** + +```json +{ + "data": { + "GetWallet": { + "account": { + "address": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //The address of the verified account + }, + "balances": { + "free": "86010842630734264894" // The amount of free ENJ in the verified account - ~86.0108 ENJ + } + } + } +} +``` + +:::info **Formatting Balances** +The API returns balance fields in the **base unit** (u128), meaning they appear as large integers without decimals. To obtain the readable ENJ amount, **divide the returned value by 10^18**. +- **Formula:** `Base Unit Value` / `1,000,000,000,000,000,000` = `ENJ Amount` +- **Example:** A value of `86010842630734264894` converts to approximately **86.01 ENJ**. +::: + +**Response: If user has not verified** + +```json +{ + "data": { + "GetWallet": null + } +} +``` + +#### Option B: `GetAccountVerified` + +This query can be used to fetch the status of the verification, while you are showing the QR code to the player, for example, when the query returns `"verified": true` you could hide the QR code and proceed with your onboarding workflow. + +**Query:** + + + +```graphql +query GetAccountVerified{ + GetAccountVerified(verificationId: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"){ #Set the verificationId from the RequestAccount response + verified + account{ + publicKey + address + } + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"query GetAccountVerified($verification_id: String!) {\r\n GetAccountVerified(verificationId: $verification_id) {\r\n verified\r\n account {\r\n publicKey\r\n address\r\n }\r\n }\r\n}","variables":{"verification_id":"0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Setup the query +var getAccountVerified = new GetAccountVerified() + .SetVerificationId("0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"); + +// Define and assign the return data fragment to the query +var accountVerifiedFragment = new AccountVerifiedFragment() + .WithVerified() + .WithAccount(new AccountFragment() + .WithPublicKey() + .WithAddress() + ); + +getAccountVerified.Fragment(accountVerifiedFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendGetAccountVerified(getAccountVerified); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreQueries.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Set up the query + GetAccountVerified getAccountVerified = GetAccountVerified() + .SetVerificationId(make_shared("0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c")); + + // Define and assign the return data fragments to the query + AccountFragment accountFragment = AccountFragment() + .WithPublicKey() + .WithAddress(); + AccountVerifiedFragment accountVerifiedFragment = AccountVerifiedFragment() + .WithVerified() + .WithAccount(make_shared(accountFragment)); + + getAccountVerified.SetFragment(make_shared(accountVerifiedFragment)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendGetAccountVerified(*client, getAccountVerified); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& getAccountVerifiedResult = gqlResult->GetData()->GetResult(); + + std::cout << getAccountVerifiedResult->GetVerified().value() << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} + +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + query GetAccountVerified($verification_id: String!) { + GetAccountVerified(verificationId: $verification_id) { + verified + account{ + publicKey + address + } + } + } + `, + variables: { + verification_id: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" //Set the verificationId from the RequestAccount response + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + query GetAccountVerified($verification_id: String!) { + GetAccountVerified(verificationId: $verification_id) { + verified + account{ + publicKey + address + } + } + } + `, + variables: { + verification_id: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" //Set the verificationId from the RequestAccount response + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +query GetAccountVerified($verification_id: String!) { + GetAccountVerified(verificationId: $verification_id) { + verified + account{ + publicKey + address + } + } +} +''' + +variables = { + 'verification_id': "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" #Set the verificationId from the RequestAccount response +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +**Response: If user has verified** + +```json +{ + "data": { + "GetAccountVerified": { + "verified": true, + "account": { + "publicKey": "0x5a6aae294416f3e875d9a8975658905002cfd3e5e64105d76296c4b0adbfd77e", //The public key of the verified account + "address": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //The address of the verified account + } + } + } +} +``` + +**Response: If user has not verified** + +```json +{ + "data": { + "GetAccountVerified": { + "verified": false, + "address": null + } + } +} +``` + +![Verify Flow Diagram](/img/guides/managing-users/verify-flow-diagram.png) + +:::info Explore More Arguments +For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. +::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/_category_.json new file mode 100644 index 0000000..7c7d976 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Connecting User Wallets" +} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md new file mode 100644 index 0000000..687e483 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md @@ -0,0 +1,915 @@ +--- +title: "Reading User Wallets" +slug: "reading-user-wallets" +description: "Explore how to read user wallet data, providing insights into user transactions, holdings, and interactions within the blockchain." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Fetching wallets is crucial because it allows you to see the contents of a wallet, including all tokens held within. + +This is especially important in gaming and app development as it enables you to assign specific utilities or benefits to certain tokens. + +For example, if a user has a particular token in their wallet, they might gain access to exclusive in-game items or features within an app, enhancing the user experience and adding value to the tokens. + +:::info What you'll need: +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +::: + +:::tip Best Practices +- Only pull data from the collections you need. This will improve performance and ensure your backend infrastructure remains scalable. +- Always read and check the balance. Sometimes, the player may appear to hold the token but the balance is actually zero, for instance, if the token has been burned. These burned tokens should not have in-game utility. +- Consider whether you want NFTs that are listed on a marketplace to have utility in your game. Items that are listed for sale don't show up in regular token balance and show up in reserved balance instead. If you want them to have utility, you can check the reserved balance and include the reserved supply. +- When an API call response contains too many results, they are paginated. Users can have hundreds of tokens, in this situation you will need to read them in multiple calls. [Read more about Pagination here](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination). +- If you plan to utilize metadata from on-chain or external sources, it's important to also read the token's attributes. Typically, you'll find a "uri" attribute that points to the external location of this metadata. +::: + +## Fetching a wallet with the [Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) + +### Fetching wallet's Enjin Coin balance + +Use the `GetWallet` query and include the the `balances` in the query response to see how much ENJ a wallet holds. + +**Query:** + + + +```graphql +query FetchWalletBalance{ + GetWallet(account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"){ #Specify the account address + balances{ + free + reserved + } + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"query FetchWalletBalance($account: String!) {\r\n GetWallet(account: $account) {\r\n balances {\r\n free\r\n reserved\r\n }\r\n }\r\n}","variables":{"account":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Setup the query +var getWallet = new GetWallet() + .SetAccount("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"); + +// Define and assign the return data fragment to the query +var walletFragment = new WalletFragment() + .WithBalances(new BalancesFragment() + .WithFree() + .WithReserved() + ); + +getWallet.Fragment(walletFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendGetWallet(getWallet); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreQueries.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Set up the query + GetWallet getWallet = GetWallet() + .SetAccount(make_shared("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f")); + + // Define and assign the return data fragments to the query + WalletFragment walletFragment = WalletFragment(); + BalancesFragment balancesFragment = BalancesFragment() + .WithFree() + .WithReserved(); + + walletFragment.WithBalances(make_shared(balancesFragment)); + + getWallet.SetFragment(make_shared(walletFragment)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendGetWallet(*client, getWallet); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& getWalletResult = gqlResult->GetData()->GetResult(); + + std::cout << getWalletResult->GetBalances()->GetFree().value() << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + query FetchWalletBalance($account: String!) { + GetWallet(account: $account) { + balances{ + free + reserved + } + } + } + `, + variables: { + account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + query FetchWalletBalance($account: String!) { + GetWallet(account: $account) { + balances{ + free + reserved + } + } + } + `, + variables: { + account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +query FetchWalletBalance($account: String!) { + GetWallet(account: $account) { + balances{ + free + reserved + } + } +} +''' + +variables = { + 'account': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the account address +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +**Response:** + +```json +{ + "data": { + "GetWallet": { + "balances": { + "free": "86010842630734264894", //~86.01084... ENJ + "reserved": "13900475000000000000" //~13.90047... ENJ + } + } + } +} +``` + +:::info **Formatting Balances** +The API returns balance fields in the **base unit** (u128), meaning they appear as large integers without decimals. To obtain the readable ENJ amount, **divide the returned value by 10^18**. +- **Formula:** `Base Unit Value` / `1,000,000,000,000,000,000` = `ENJ Amount` +- **Example:** A value of `86010842630734264894` converts to approximately **86.01 ENJ**. +::: + +### Fetch a wallet's collections + +Use the `GetWallet` query and include the the `collectionAccounts` in the query response to see what collections a wallet holds. + +**Query:** + + + +```graphql +query FetchingWalletCollections{ + GetWallet(account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"){ #Specify the account address + collectionAccounts{ + edges{ + node{ + collection{ + collectionId + attributes{ + key + value + } + } + } + } + } + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"query FetchingWalletCollections($account: String!) {\r\n GetWallet(account: $account) {\r\n collectionAccounts {\r\n edges {\r\n node {\r\n collection {\r\n collectionId\r\n attributes {\r\n key\r\n value\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}","variables":{"account":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Setup the query +var getWallet = new GetWallet() + .SetAccount("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"); + +// Define and assign the return data fragment to the query +var walletFragment = new WalletFragment() + .WithCollectionAccounts(new ConnectionFragment() + .WithEdges(new EdgeFragment() + .WithNode(new CollectionAccountFragment() + .WithCollection(new CollectionFragment() + .WithCollectionId() + .WithAttributes(new AttributeFragment() + .WithKey() + .WithValue() + ) + ) + ) + ) + ); + +getWallet.Fragment(walletFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendGetWallet(getWallet); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreQueries.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Set up the query + GetWallet getWallet = GetWallet() + .SetAccount(make_shared("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f")); + + // Define and assign the return data fragments to the query + AccountFragment accountFragment = AccountFragment() + .WithAddress(); + WalletFragment walletFragment = WalletFragment() + .WithAccount(make_shared(accountFragment)); + AttributeFragment attributeFragment = AttributeFragment() + .WithKey() + .WithValue(); + CollectionFragment collectionFragment = CollectionFragment() + .WithCollectionId() + .WithAttributes(make_shared(attributeFragment)); + CollectionAccountFragment collectionAccountFragment = CollectionAccountFragment() + .WithCollection(make_shared(collectionFragment)); + EdgeFragment edgeFragment = EdgeFragment() + .WithNode(make_shared(collectionAccountFragment)); + + ConnectionFragment connectionFragment = ConnectionFragment() + .WithEdges(make_shared>(edgeFragment)); + + walletFragment.WithCollectionAccounts(make_shared>(connectionFragment)); + + getWallet.SetFragment(make_shared(walletFragment)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendGetWallet(*client, getWallet); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& getWalletResult = gqlResult->GetData()->GetResult(); + + std::cout << getWalletResult->GetAccount()->GetAddress().value() << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + query FetchingWalletCollections($account: String!) { + GetWallet(account: $account) { + collectionAccounts{ + edges{ + node{ + collection{ + collectionId + attributes{ + key + value + } + } + } + } + } + } + } + `, + variables: { + account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + query FetchingWalletCollections($account: String!) { + GetWallet(account: $account) { + collectionAccounts{ + edges{ + node{ + collection{ + collectionId + attributes{ + key + value + } + } + } + } + } + } + } + `, + variables: { + account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +query FetchingWalletCollections($account: String!) { + GetWallet(account: $account) { + collectionAccounts{ + edges{ + node{ + collection{ + collectionId + attributes{ + key + value + } + } + } + } + } + } +} +''' + +variables = { + 'account': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the account address +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +**Response:** + +```json +{ + "data": { + "GetWallet": { + "collectionAccounts": { + "edges": [ + { + "node": { + "collection": { + "collectionId": "33866", + "attributes": [] + } + } + }, + { + "node": { + "collection": { + "collectionId": "36105", + "attributes": [ + { + "key": "name", + "value": "My test collection" + } + ] + } + } + }, + { + "node": { + "collection": { + "collectionId": "36623", + "attributes": [ + { + "key": "name", + "value": "Docs Testing Collection" + } + ] + } + } + } + ] + } + } + } +} +``` + +:::tip Using Pagination +The response may be displayed on several pages. To view all of it, you may need to follow steps for [pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination) which allows you to flip through the pages. +::: + +### Fetch a wallet's tokens + +Use the `GetWallet` query and include the the `tokenAccounts` in the query response to see what tokens a wallet holds. + +**Query:** + + + +```graphql +query FetchingWalletTokens{ + GetWallet(account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"){ #Specify the account address + tokenAccounts{ + edges{ + node{ + balance + token{ + tokenId + collection{ + collectionId + } + attributes{ + key + value + } + } + } + } + } + } +} +``` + + +``` +curl -X POST "https://platform.enjin.io/graphql" \ +-H "Content-Type: application/json" \ +-H "Authorization: enjin_api_key" \ +-d '{"query": "query FetchingWalletTokens { GetWallet(account: \"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f\") { tokenAccounts { edges { node { balance token { tokenId collection { collectionId } attributes { key value } } } } } } }"}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Setup the query +var getWallet = new GetWallet() + .SetAccount("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"); + +// Define and assign the return data fragment to the query +var walletFragment = new WalletFragment() + .WithTokenAccounts(new ConnectionFragment() + .WithEdges(new EdgeFragment() + .WithNode(new TokenAccountFragment() + .WithBalance() + .WithToken(new TokenFragment() + .WithTokenId() + .WithCollection(new CollectionFragment() + .WithCollectionId() + ) + .WithAttributes(new AttributeFragment() + .WithKey() + .WithValue() + ) + ) + ) + ) + ); + +getWallet.Fragment(walletFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendGetWallet(getWallet); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreQueries.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Set up the query + GetWallet getWallet = GetWallet() + .SetAccount(make_shared("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f")); + + // Define and assign the return data fragments to the query + AccountFragment accountFragment = AccountFragment() + .WithAddress(); + WalletFragment walletFragment = WalletFragment() + .WithAccount(make_shared(accountFragment)); + AttributeFragment attributeFragment = AttributeFragment() + .WithKey() + .WithValue(); + CollectionFragment collectionFragment = CollectionFragment() + .WithCollectionId(); + TokenFragment tokenFragment = TokenFragment() + .WithTokenId() + .WithCollection(make_shared(collectionFragment)) + .WithAttributes(make_shared(attributeFragment)); + TokenAccountFragment tokenAccountFragment = TokenAccountFragment() + .WithBalance() + .WithToken(make_shared(tokenFragment)); + + EdgeFragment edgeFragment = EdgeFragment() + .WithNode(make_shared(tokenAccountFragment)); + + ConnectionFragment connectionFragment = ConnectionFragment() + .WithEdges(make_shared>(edgeFragment)); + + walletFragment.WithTokenAccounts(make_shared>(connectionFragment)); + + getWallet.SetFragment(make_shared(walletFragment)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendGetWallet(*client, getWallet); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& getWalletResult = gqlResult->GetData()->GetResult(); + + std::cout << getWalletResult->GetAccount()->GetAddress().value() << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + query FetchingWalletTokens($account: String!) { + GetWallet(account: $account) { + tokenAccounts{ + edges{ + node{ + balance + token{ + tokenId + collection{ + collectionId + } + attributes{ + key + value + } + } + } + } + } + } + } + `, + variables: { + account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + query FetchingWalletTokens($account: String!) { + GetWallet(account: $account) { + tokenAccounts{ + edges{ + node{ + balance + token{ + tokenId + collection{ + collectionId + } + attributes{ + key + value + } + } + } + } + } + } + } + `, + variables: { + account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +query FetchingWalletTokens($account: String!) { + GetWallet(account: $account) { + tokenAccounts{ + edges{ + node{ + balance + token{ + tokenId + collection{ + collectionId + } + attributes{ + key + value + } + } + } + } + } + } +} +''' + +variables = { + 'account': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the account address +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +**Response:** + +```json +{ + "data": { + "GetWallet": { + "tokenAccounts": { + "edges": [ + { + "node": { + "balance": "1", + "token": { + "tokenId": "0", + "collection": { + "collectionId": "33866" + }, + "attributes": [] + } + } + }, + { + "node": { + "balance": "1", + "token": { + "tokenId": "0", + "collection": { + "collectionId": "36105" + }, + "attributes": [ + { + "key": "Name", + "value": "Awesome Token!" + } + ] + } + } + }, + { + "node": { + "balance": "6", + "token": { + "tokenId": "1", + "collection": { + "collectionId": "36105" + }, + "attributes": [ + { + "key": "name", + "value": "Awesome Token 2!" + } + ] + } + } + } + ] + } + } + } +} +``` + +:::tip Using Pagination +The response may be displayed on several pages. To view all of it, you may need to follow steps for [pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination) which allows you to flip through the pages. +::: + +:::info Explore More Arguments +For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. +::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md new file mode 100644 index 0000000..f6314e5 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md @@ -0,0 +1,975 @@ +--- +title: "Using Managed Wallets" +slug: "using-managed-wallets" +description: "Learn how to use managed wallets within the Enjin ecosystem, allowing for user-friendly blockchain wallet management and asset handling." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Managed wallets provide a seamless experience for your users by allowing you to create and manage blockchain wallets on their behalf. Users don't need to download any special apps or take extra steps, making it easier for them to get started with your application. + +This approach offers transparency, allowing users to easily track their digital assets. It simplifies user onboarding and provides clear, immutable on-chain data for tracking tokens, enhancing trust and engagement within your platform. + +Here's how the typical flow works: + +1. **[Managed Wallet Creation](#create-managed-wallets):** When a new user starts using your application, you initiate the creation of a managed wallet for them, and assign the user's internal ID from your database to the newly created managed wallet. This ensures a consistent link between the user and their wallet. +2. **[Asset Management](#minting-tokens-to-managed-wallets):** As the user earns or deposits digital assets within your application, these assets are automatically stored in their managed wallet. +3. **[In-App Transactions](#minting-tokens-to-managed-wallets):** Any actions the user performs within your application that involve their wallet (e.g., spending tokens, participating in in-game economies) are executed directly on their managed wallet. +4. **[User Withdrawal (Optional)](#transferring-tokens-from-managed-wallets):** If a user decides they want to take full control of their assets, you can facilitate the transfer of all funds from their managed wallet to their own self-custodial wallet. + +This process ensures that users can interact with blockchain assets seamlessly within your application without needing to understand the underlying complexities of blockchain technology or operating his own crypto wallet. + +:::info What you'll need: +- Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for . +You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to add to the wallet. +::: + +--- + +## Create Managed Wallets + +To create a Managed wallet, run the `CreateWallet` mutation, with a unique ID as a parameter. + +Choose a unique `externalId` for each player/user that can be cross-referenced later. This unique identifier should be something already associated with the player in your database, such as a player ID or username. +By doing so, you will be able to consistently link the Managed Wallet to the respective player. + + + + +```graphql +mutation CreateManagedWallet { + CreateWallet(externalId: "player_1_id") #Replace this with a unique ID +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation CreateManagedWallet($external_id: String!) {\r\n CreateWallet(externalId: $external_id)\r\n}","variables":{"externalId":"player_1_id"}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Setup the mutation +var createWallet = new CreateWallet() + .SetExternalId("player_1_id"); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendCreateWallet(createWallet); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreMutations.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Set up the query + CreateWallet createWallet = CreateWallet() + .SetExternalId(make_shared("player_1_id")); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendCreateWallet(*client, createWallet); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& getCreateWalletResult = gqlResult->GetData()->GetResult(); + + std::cout << getCreateWalletResult.value() << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation CreateManagedWallet($external_id: String!) { + CreateWallet(externalId: $external_id) + } + `, + variables: { + externalId: "player_1_id" //Replace this with a unique ID + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation CreateManagedWallet($external_id: String!) { + CreateWallet(externalId: $external_id) + } + `, + variables: { + externalId: "player_1_id" //Replace this with a unique ID + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation CreateManagedWallet($external_id: String!) { + CreateWallet(externalId: $external_id) +} +''' + +variables = { + 'externalId': "player_1_id" #Replace this with a unique ID +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +:::warning Lost database data? +Recreate the Managed wallets by running `CreateWallet` mutation again for each of the `externalId`s. +Make sure to use the same Daemon wallet seed and password used to create Managed wallets prior, as Managed wallets are derived with the following derivation path: `walletSeed/externalId///password` +::: + +## Interact with Managed Wallets + +Once the Managed wallet is created, you can provide the `externalId` field of the Wallet object to any query or mutation that accepts a `Wallet.externalId` parameter, in order to use a Managed wallet instead of any other wallet. + +Lets look at the `GetWallet` query as an example, to [get the Public Key of an account](/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md), we use the account's address. `account: "address_here"` + +However, to get the Public Key of a `Managed wallet`, we provide the `Wallet.externalId` parameter instead. + +**Query:** + + + +```graphql +query GetManagedWalletPublicKey { + GetWallet(externalId: "player_1_id") { #Specify the managed wallet unique ID + account { + address + publicKey + } + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"query GetManagedWalletPublicKey($external_id: String!) {\r\n GetWallet(externalId: $external_id) {\r\n account {\r\n address\r\n publicKey\r\n }\r\n }\r\n}","variables":{"externalId":"player_1_id"}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Setup the query +var getWallet = new GetWallet() + .SetExternalId("player_1_id"); + +// Define and assign the return data fragment to the query +var walletFragment = new WalletFragment() + .WithAccount(new AccountFragment() + .WithAddress() + .WithPublicKey() + ); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendGetWallet(getWallet); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreQueries.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Set up the query + GetWallet getWallet = GetWallet() + .SetExternalId(make_shared("player_1_id")); + + // Define and assign the return data fragments to the query + AccountFragment accountFragment = AccountFragment() + .WithAddress() + .WithPublicKey(); + WalletFragment walletFragment = WalletFragment() + .WithAccount(make_shared(accountFragment)); + + getWallet.SetFragment(make_shared(walletFragment)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendGetWallet(*client, getWallet); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& getWalletResult = gqlResult->GetData()->GetResult(); + + std::cout << getWalletResult->GetAccount()->GetAddress().value() << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + query GetManagedWalletPublicKey($external_id: String!) { + GetWallet(externalId: $external_id) { + account { + address + publicKey + } + } + } + `, + variables: { + externalId: "player_1_id" //Specify the managed wallet unique ID + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + query GetManagedWalletPublicKey($external_id: String!) { + GetWallet(externalId: $external_id) { + account { + address + publicKey + } + } + } + `, + variables: { + externalId: "player_1_id" //Specify the managed wallet unique ID + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +query GetManagedWalletPublicKey($external_id: String!) { + GetWallet(externalId: $external_id) { + account { + address + publicKey + } + } +} +''' + +variables = { + 'externalId': "player_1_id" #Specify the managed wallet unique ID +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +**Response:** + +```json +{ + "data": { + "GetWallet": { + "account": { + "address": "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT", //Account address for Managed wallet ID "player_1_id" + "publicKey": "0x92f33d3efd6af37798b125cba08e21fc7b404293f12c067f1cb6ab326775ff08" + } + } + } +} +``` + +## Minting tokens to Managed wallets + +With the acquired Managed wallet address, you can mint tokens directly to the Managed wallet. + + + +```graphql +mutation BatchMint { + BatchMint( + collectionId: 7154 #Specify the collection ID + recipients: [ + { + account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" #The recipient of the mint (the Managed wallet account address from the GetWallet query) + mintParams: { + amount:1 #Amount to mint + tokenId: {integer: 6533} #Token ID to mint + } + } + ] + ) { + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation BatchMint($collection_id: BigInt!) {\r\n BatchMint(\r\n collectionId: $collection_id\r\n recipients: [\r\n {\r\n account: \"cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT\" #The recipient of the mint (the Managed wallet account address from the GetWallet query)\r\n mintParams: {\r\n amount: 1 #Amount to mint\r\n tokenId: { integer: 6533 } #Token ID to mint\r\n }\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":7154}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Create the list of recipients for the batch mint +var recipients = new List() +{ + new MintRecipient() + .SetAccount("cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT") + .SetMintParams(new MintTokenParams() + .SetAmount(1) + .SetTokenId(new EncodableTokenIdInput().SetInteger(6533) + ) + ) +}; + +// Setup the mutation +var batchMint = new BatchMint() + .SetCollectionId(7154) + .SetRecipients(recipients.ToArray()); + +// Define and assign the return data fragment to the mutation +var transactionFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +batchMint.Fragment(transactionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendBatchMint(batchMint); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreMutations.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Set up the transaction parameters + EncodableTokenIdInput token1 = EncodableTokenIdInput() + .SetInteger(make_shared("6533")); + + MintTokenParams mintTokenParams = MintTokenParams() + .SetAmount(make_shared("1")) + .SetTokenId(make_shared(token1)); + + MintRecipient mintRecipient = MintRecipient() + .SetAccount(make_shared("cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT")) + .SetMintParams(make_shared(mintTokenParams)); + + vector recipients; + recipients.push_back(mintRecipient); + + // Set up the mutation + BatchMint batchMint = BatchMint() + .SetCollectionId(make_shared("7154")) + .SetRecipients(make_shared>(vector{mintRecipient})); + + TransactionFragment transactionFragment = TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + + batchMint.SetFragment(make_shared(transactionFragment)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendBatchMint(*client, batchMint); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& batchMintResult = gqlResult->GetData()->GetResult(); + + std::cout << ToString(batchMintResult->GetMethod().value()) << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation BatchMint($collection_id: BigInt!) { + BatchMint( + collectionId: $collection_id + recipients: [ + { + account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" #The recipient of the mint (the Managed wallet account address from the GetWallet query) + mintParams: { + amount:1 #Amount to mint + tokenId: {integer: 6533} #Token ID to mint + } + } + ] + ) { + id + method + state + } + } + `, + variables: { + collection_id: 7154 //Specify the managed wallet unique ID + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation BatchMint($collection_id: BigInt!) { + BatchMint( + collectionId: $collection_id + recipients: [ + { + account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" #The recipient of the mint (the Managed wallet account address from the GetWallet query) + mintParams: { + amount:1 #Amount to mint + tokenId: {integer: 6533} #Token ID to mint + } + } + ] + ) { + id + method + state + } + } + `, + variables: { + collection_id: 7154 //Specify the managed wallet unique ID + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation BatchMint($collection_id: BigInt!) { + BatchMint( + collectionId: $collection_id + recipients: [ + { + account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" #The recipient of the mint (the Managed wallet account address from the GetWallet query) + mintParams: { + amount:1 #Amount to mint + tokenId: {integer: 6533} #Token ID to mint + } + } + ] + ) { + id + method + state + } +} +''' + +variables = { + 'collection_id': 7154 #Specify the managed wallet unique ID +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +## Transferring tokens from managed wallets + +If you followed along the previous snippets of code, you should have a Managed wallet with a token in it. To transfer it out to another wallet, we can use the `BatchTransfer` mutation. + +:::info Signing transactions using a managed wallet +It's important to note that every mutation that accepts a `signingAccount` argument can be used to sign the transaction using a managed wallet, as shown in the batch transfer tutorial below. +::: + + + +```graphql +mutation BatchTransfer { + BatchTransfer( + collectionId: 7154 #Specify the collection ID + signingAccount: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" # Add your signing account address (the Managed wallet account address from the GetWallet query) + recipients: [ + { + account: "cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ" #The recipient of the transfer + simpleParams: { + tokenId: { integer: 6533 } #Token ID to transfer + amount: 1 #Amount to transfer + } + } + ] + ) { + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation BatchTransfer($collection_id: BigInt!, $signing_account: String!) {\r\n BatchTransfer(\r\n collectionId: $collection_id\r\n signingAccount: $signing_account\r\n recipients: [\r\n {\r\n account: \"cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ\" #The recipient of the transfer\r\n simpleParams: {\r\n tokenId: { integer: 6533 } #Token ID to transfer\r\n amount: 1 #Amount to transfer\r\n }\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":7154,"signing_account":"cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT"}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Create the array of recipients for the batch transfer +var recipients = new List() +{ + new TransferRecipient() + .SetAccount("cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ") // The recipient of the transfer + .SetSimpleParams(new SimpleTransferParams() + .SetTokenId(new EncodableTokenIdInput().SetInteger(6533)) + .SetAmount(1) + ) +}; + +// Setup the mutation +var batchTransfer = new BatchTransfer() + .SetCollectionId(7154) + .SetSigningAccount("cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT") // Add your signing account address (the Managed wallet account address from the GetWallet query) + .SetRecipients(recipients.ToArray()); + +// Define and assign the return data fragment to the mutation +var transactionFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +batchTransfer.Fragment(transactionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendBatchTransfer(batchTransfer); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreMutations.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +int main() { + + // Set up the transaction paramters + EncodableTokenIdInput token1 = EncodableTokenIdInput() + .SetInteger(make_shared("6533")); + + SimpleTransferParams simpleTransferParams = SimpleTransferParams() + .SetAmount(make_shared("1")) + .SetTokenId(make_shared(token1)); + + TransferRecipient transferRecipient = TransferRecipient() + .SetAccount(make_shared("cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ")) + .SetSimpleParams(make_shared(simpleTransferParams)); + + vector recipients; + recipients.push_back(transferRecipient); + + // Set up the mutation + BatchTransfer batchTransfer = BatchTransfer() + .SetCollectionId(make_shared("7154")) + .SetSigningAccount(make_shared("cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT")) + .SetRecipients(make_shared>(recipients)); + + TransactionFragment transactionFragment = TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + + batchTransfer.SetFragment(make_shared(transactionFragment)); + + // Create and auth a client to send the request to the platform + unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); + client->Auth("Your_Platform_Token_Here"); + + // Send the request then get the response and write the output to the console. + // Only the fields that were requested in the fragment will be filled in, + // other fields which weren't requested in the fragment will be set to null. + future>> futureResponse = SendBatchTransfer(*client, batchTransfer); + + // Get the platform response holding the HTTP data + PlatformResponsePtr> response = futureResponse.get(); + + // Get the result, a GraphQL response, holding the GraphQL data + const optional>& gqlResult = response->GetResult(); + + // Write the result data to the console + if (gqlResult.has_value() && gqlResult->IsSuccess()) + { + const optional& batchTransferResult = gqlResult->GetData()->GetResult(); + + std::cout << ToString(batchTransferResult->GetMethod().value()) << std::endl; + } + + // Write any error messages to the console + if (gqlResult.has_value() && gqlResult->HasErrors()) + { + const optional>& errors = gqlResult->GetErrors(); + + for (const GraphQlError& error : errors.value()) { + std::cout << error.GetMessage().value() << std::endl; + } + } + + client.reset(); + + return 0; +} +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation BatchTransfer + ( + $collection_id: BigInt! + $signing_account: String! + ) { + BatchTransfer( + collectionId: $collection_id + signingAccount: $signing_account + recipients: [ + { + account: "cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ" #The recipient of the transfer + simpleParams: { + tokenId: { integer: 6533 } #Token ID to transfer + amount: 1 #Amount to transfer + } + } + ] + ) { + id + method + state + } + } + `, + variables: { + collection_id: 7154, //Specify the collection ID + signing_account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" //Add your signing account address (the Managed wallet account address from the GetWallet query) + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation BatchTransfer + ( + $collection_id: BigInt! + $signing_account: String! + ) { + BatchTransfer( + collectionId: $collection_id + signingAccount: $signing_account + recipients: [ + { + account: "cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ" #The recipient of the transfer + simpleParams: { + tokenId: { integer: 6533 } #Token ID to transfer + amount: 1 #Amount to transfer + } + } + ] + ) { + id + method + state + } + } + `, + variables: { + collection_id: 7154, //Specify the collection ID + signing_account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" //Add your signing account address (the Managed wallet account address from the GetWallet query) + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation BatchTransfer +( + $collection_id: BigInt! + $signing_account: String! +) { + BatchTransfer( + collectionId: $collection_id + signingAccount: $signing_account + recipients: [ + { + account: "cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ" #The recipient of the transfer + simpleParams: { + tokenId: { integer: 6533 } #Token ID to transfer + amount: 1 #Amount to transfer + } + } + ] + ) { + id + method + state + } +} +''' + +variables = { + 'collection_id': 7154, #Specify the collection ID + 'signing_account': "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" #Add your signing account address (the Managed wallet account address from the GetWallet query) +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +Make sure that `signingAccount` is set to the Managed Wallet address that owns that token. + +:::info Explore More Arguments +For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. +::: \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md new file mode 100644 index 0000000..c63eae4 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md @@ -0,0 +1,1057 @@ +--- +title: "Using Fuel Tanks" +slug: "using-fuel-tanks" +description: "Discover how to use fuel tanks to cover blockchain transaction fees, creating a seamless and cost-free experience for users within your application." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +A Fuel tank is a pool that holds Enjin Coins (ENJ) which is used to cover transaction fees for eligible users. Each is designed to minimize transaction costs, resulting in lower expenses for platform operations. + +Fuel Tanks are flexible and dynamic, allowing customization based on specific rules and requirements. They operate based on a set of rules, making it possible to target specific actions or accounts meeting specific criteria. + +When dispatching a transaction via a Fuel Tank, the following checks must pass: + +- The Fuel Tank is not frozen. +- The Fuel Tank is funded with enough s. +- The dispatcher account is allowed to use the Fuel Tank. +- The Fuel Tank allows dispatching this transaction. + +:::info What you'll need: +- Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin / Canary Matrixchain to pay for , and for funding the tank. +You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +::: + +## Tank Rules + +Fuel Tanks offer versatile rules for various use cases, allowing customization. + +### Dispatch Rules + +#### Rule Set + +When a call is made to a fuel tank (also known as "Dispatching"), it must be made in accordance with a set of rules. These rules, known as rule sets, can include multiple individual Dispatch Rules that determine the validity of the call. A fuel tank can have multiple rule sets, each of which controls access and permissions to the fuel tank's functionality and resources. + +#### Types of Dispatch Rules: + +- **`Whitelisted Callers`**: Subsidize s dispatched from certain s. +- **`Whitelisted Collections`**: Subsidize s involving specific . +- **`Require Token`**: Subsidize s only if the user holds a specific NFT. +- **`WhitelistedPallets`**: Subsidize s involving specific . +- **`Permitted Extrinsics`**: Subsidize s involving specific s, e.g. marketplace CreateListing transactions. +- **`Permitted Calls`**: Subsidize s involving specific s and parameters. +- **`User Fuel Budget`**: Set limits on individual fuel consumption for security. +- **`Tank Fuel Budget`**: Define collective fuel usage to extend Fuel Tank lifespan. +- **`Max Fuel Burn Per Transaction`**: Control fuel consumption per transaction for predictability. +- **`Require Signature`**: Subsidize s which were signed by a specific . + +These rules enhance the Enjin Blockchain experience, sparking new products and business models. + +#### RequireAccount Parameter + +In addition to Dispatch Rules, a Rule Set may require the dispatching account to have an existing Fuel Tank User Account in order to dispatch a call. This is configured by setting the Rule Set's `requireAccount` parameter to true. + +### Account Rules + +Adding an account to the Fuel Tank User Accounts can be done in two ways: + +1. In advance with the `add_account` extrinsic. +2. When dispatching a call with the `dispatch_and_touch` extrinsic. + +In both ways, the account is added to the tank's User Accounts only if it's successfully validated by the tank's Account Rules. + +Note: if the dispatcher is not the tank owner, he may add itself to the User Accounts only if [UserAccountManagement](#user-account-management) is configured + +#### Types of Account Rules: + +- **`Whitelisted Callers`**: Only listed accounts are able to add their account to the Fuel Tank's User Accounts. +- **`Require Token`**: Only accounts that hold the specified token are able to add their account to the Fuel Tank's User Accounts. + +### User Account Management + +By default, only the Fuel Tank owner has the permission to add accounts to the Fuel Tank User Accounts. However, the Fuel Tank can be configured to allow accounts to add themselves by setting the `tankReservesAccountCreationDeposit` argument. + +- **Default Behavior**: If the `tankReservesAccountCreationDeposit` argument is not provided, only the Fuel Tank owner can add accounts to the Fuel Tank User Accounts. +- **Self-Addition without Deposit Funding**: If the `tankReservesAccountCreationDeposit` argument is set to `false`, the Fuel Tank allows accounts to add themselves to the Fuel Tank User Accounts. However, the Fuel Tank will not provide the funds required for the Tank User Account . +- **Self-Addition with Deposit Funding**: If the `tankReservesAccountCreationDeposit` argument is set to `true`, the Fuel Tank allows accounts to add themselves to the Fuel Tank User Accounts, and it also covers the necessary funds required for the Tank User Account . + +### Coverage Policy + +By default, the fuel tank will subsidize only . +To cover both and any the dispatched call may require, set the Coverage Policy to `FEES_AND_DEPOSIT` + +## Creating Fuel Tanks + +Now that we have a basic understanding of how Fuel Tanks are structured, let's go through setting up some Fuel Tanks with specific functionality, including creating rules and customizing it to fit your needs, enabling you to reduce transaction costs for your users. + +:::info User Interface +This guide will demonstrate how to create various fuel tanks with the GraphQL API. +Note that you may also use the User Interface to create Fuel Tanks. + +To create a Fuel Tank using the Platform's User Interface, navigate to "**[Fuel Tanks](https://platform.canary.enjin.io/fuel-tanks)**" in the Platform Menu. Then, click the "**[Create Fuel Tank](https://platform.canary.enjin.io/create/fuel-tank)**" button. +::: + +:::warning **Feature Availability Notice** +Please be aware that the following arguments are currently unavailable on the Enjin Platform: +- `PermittedExtrinsics` cannot be set via the Enjin Platform UI. +- `WhitelistedPallets` is not supported on the Enjin Platform, both on the UI and API. +- `requireAccount` is also not supported on the Enjin Platform, both on the UI and API. +We will update the documentation once these options are available. +Code snippets with these arguments are provided for illustrative purposes only. If you wish to create a fuel tank with these options now, please use the Enjin Console at [console.enjin.io](https://console.enjin.io). +::: + +:::info Make sure to use the Fuel Tank Endpoint +- Testnet: `http://platform.canary.enjin.io/graphql/fuel-tanks` +- Mainnet: `http://platform.enjin.io/graphql/fuel-tanks` +**Try the Beam Playground here:** +- Testnet: http://platform.canary.enjin.io/graphiql/fuel-tanks +- Mainnet: http://platform.enjin.io/graphiql/fuel-tanks +::: + +### Subsidize Token Transfers For A Collection + +The following mutation will set up a fuel tank that only subsidizes transactions that contain a `BatchTransfer` extrinsic, and only if it involves a token from the collection with ID 36,105. + + + +```graphql +mutation CreateFuelTank{ + CreateFuelTank( + name: "Collection Token Transfers" #Specify the Fuel Tank name + coveragePolicy: FEES #This is set to FEES since we only want to subsidize transaction fees, and we don't need to provide storage deposits for token transfer transactions + reservesAccountCreationDeposit: null #This is set to null since we don't want to allow tank account creations + dispatchRules: [{ + permittedExtrinsics: [BatchTransfer] #This rule specifies that only batch transfers are subsidized + whitelistedCollections: [36105] #This rule ensures that only the specified collection is subsidized + }] + ){ + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation CreateFuelTank(\r\n $name: String!\r\n $coveragePolicy: CoveragePolicy\r\n $permittedExtrinsics: [TransactionMethod!]\r\n $whitelistedCollections: [BigInt!]\r\n) {\r\n CreateFuelTank(\r\n name: $name\r\n coveragePolicy: $coveragePolicy\r\n dispatchRules: [\r\n {\r\n permittedExtrinsics: $permittedExtrinsics #This rule specifies that only batch transfers are subsidized\r\n whitelistedCollections: $whitelistedCollections #This rule ensures that only the specified collection is subsidized\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"name":"Collection Token Transfers","coveragePolicy":"FEES","permittedExtrinsics":["BatchTransfer"],"whitelistedCollections":[36105]}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; +using Enjin.Platform.Sdk.FuelTanks; + +// Create the array of dispatch rules for the fuel tank +var dispatchRules = new List() +{ + new DispatchRuleInputType() + .SetPermittedExtrinsics(TransactionMethod.BatchTransfer) // This rule specifies that only batch transfers are subsidized + .SetWhitelistedCollections(36105) // This rule ensures that only the specified collection is subsidized +}; + +// Setup the mutation +var createFuelTank = new CreateFuelTank() + .SetName("Collection Token Transfers") // Set the name of the fuel tank + .SetCoveragePolicy(CoveragePolicy.Fees) // This is set to FEES since we only want to subsidize transaction fees, and we don't need to provide storage deposits for token transfer transactions + .SetReservesAccountCreationDeposit(null) // This is set to null since we don't want to allow tank account creations + .SetDispatchRules(dispatchRules.ToArray()); + +// Define and assign the return data fragment to the mutation +var transactionFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +createFuelTank.Fragment(transactionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendCreateFuelTank(createFuelTank); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +// Coming Soon! +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation CreateFuelTank + ( + $name: String! + $coveragePolicy: CoveragePolicy + $permittedExtrinsics: [TransactionMethod!] + $whitelistedCollections: [BigInt!] + ) { + CreateFuelTank( + name: $name + coveragePolicy: $coveragePolicy + dispatchRules: [{ + permittedExtrinsics: $permittedExtrinsics #This rule specifies that only batch transfers are subsidized + whitelistedCollections: $whitelistedCollections #This rule ensures that only the specified collection is subsidized + }] + ){ + id + method + state + } + } + `, + variables: { + name: "Collection Token Transfers", //Specify the Fuel Tank name + coveragePolicy: "FEES", //This is set to FEES since we only want to subsidize transaction fees, and we don't need to provide storage deposits for token transfer transactions + permittedExtrinsics: ["BatchTransfer"], //This rule specifies that only batch transfers are subsidized + whitelistedCollections: [36105] //This rule ensures that only the specified collection is subsidized + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation CreateFuelTank + ( + $name: String! + $coveragePolicy: CoveragePolicy + $permittedExtrinsics: [TransactionMethod!] + $whitelistedCollections: [BigInt!] + ) { + CreateFuelTank( + name: $name + coveragePolicy: $coveragePolicy + dispatchRules: [{ + permittedExtrinsics: $permittedExtrinsics #This rule specifies that only batch transfers are subsidized + whitelistedCollections: $whitelistedCollections #This rule ensures that only the specified collection is subsidized + }] + ){ + id + method + state + } + } + `, + variables: { + name: "Collection Token Transfers", //Specify the Fuel Tank name + coveragePolicy: "FEES", //This is set to FEES since we only want to subsidize transaction fees, and we don't need to provide storage deposits for token transfer transactions + permittedExtrinsics: ["BatchTransfer"], //This rule specifies that only batch transfers are subsidized + whitelistedCollections: [36105] //This rule ensures that only the specified collection is subsidized + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation CreateFuelTank + ( + $name: String! + $coveragePolicy: CoveragePolicy + $permittedExtrinsics: [TransactionMethod!] + $whitelistedCollections: [BigInt!] + ) { + CreateFuelTank( + name: $name + coveragePolicy: $coveragePolicy + dispatchRules: [{ + permittedExtrinsics: $permittedExtrinsics #This rule specifies that only batch transfers are subsidized + whitelistedCollections: $whitelistedCollections #This rule ensures that only the specified collection is subsidized + }] + ){ + id + method + state + } + } +''' + +variables = { + 'name': "Collection Token Transfers", #Specify the Fuel Tank name + 'coveragePolicy': "FEES", #This is set to FEES since we only want to subsidize transaction fees, and we don't need to provide storage deposits for token transfer transactions + 'permittedExtrinsics': ["BatchTransfer"], #This rule specifies that only batch transfers are subsidized + 'whitelistedCollections': [36105] #This rule ensures that only the specified collection is subsidized +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +### Subsidize Any Transaction Involving s From A Certain Collection + +:::warning Upcoming Feature Notice: `WhitelistedPallets` Option +The `WhitelistedPallets` argument is not yet supported on the Enjin Platform. +The following code snippet is provided for illustrative purposes only. +If you wish to create a fuel tank with this option now, please use the Enjin Console at [console.enjin.io](https://console.enjin.io). +We will update the documentation once this option is available. +::: + +The following mutation will set up a fuel tank that only subsidizes transactions that involves any token from the collection with ID 36,105. To achieve this, we are using the same dispatch rule from the previous example (`whitelistedCollections`) and the `WhitelistedPallets` rule, allowing only extrinsics from the `MultiTokens` pallet (for actions such as `send`, `mint`, `transfer`, etc.) and `Marketplace` pallet (for actions such as `list`, `buy`, `offer`, etc. ). + + + +```graphql +mutation CreateFuelTank{ + CreateFuelTank( + name: "Collection Token Actions" #Specify the Fuel Tank name + coveragePolicy: FEES_AND_DEPOSIT #This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). + reservesAccountCreationDeposit: true #This is set to true since we want to allow tank account creations, and we want the tank to subsidize the ENJ required for the required for the Tank User Account storage deposit + dispatchRules: [{ + WhitelistedPallets: [MultiTokens, Marketplace] #This rule specifies that only extrinsics from the MultiTokens and Marketplace pallets are subsidized + whitelistedCollections: [36105] #This rule ensures that only the specified collection is subsidized + }] + ){ + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation CreateFuelTank(\r\n $name: String!\r\n $coveragePolicy: CoveragePolicy\r\n $reserveAccountCreationDeposit: Boolean\r\n $whitelistedPallets: [String!]\r\n $whitelistedCollections: [BigInt!]\r\n) {\r\n CreateFuelTank(\r\n name: $name\r\n coveragePolicy: $coveragePolicy\r\n reservesAccountCreationDeposit: $reserveAccountCreationDeposit\r\n dispatchRules: [\r\n {\r\n whitelistedPallets: $whitelistedPallets\r\n whitelistedCollections: $whitelistedCollections\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"name":"Collection Token Actions","coveragePolicy":"FEES_AND_DEPOSIT","reserveAccountCreationDeposit":true,"whitelistedPallets":["MultiTokens","Marketplace"],"whitelistedCollections":[36105]}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; +using Enjin.Platform.Sdk.FuelTanks; + +// Create the array of dispatch rules for the fuel tank +var dispatchRules = new List() +{ + new DispatchRuleInputType() + .SetWhitelistedPallets(Pallets.MultiTokens, Pallets.Marketplace) // Note: This is not yet implemented in the Platform API. This rule specifies that only extrinsics from the MultiTokens and Marketplace pallets are subsidized. + .SetWhitelistedCollections(36105) // This rule ensures that only the specified collection is subsidized. +}; + +// Setup the mutation +var createFuelTank = new CreateFuelTank() + .SetName("Collection Token Actions") // Set the name of the fuel tank + .SetCoveragePolicy(CoveragePolicy.FeesAndDeposit) // This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). + .SetReservesAccountCreationDeposit(true) // This is set to true since we want to allow tank account creations, and we want the tank to subsidize the ENJ required for the required for the Tank User Account storage deposit. + .SetDispatchRules(dispatchRules.ToArray()); + +// Define and assign the return data fragment to the mutation +var transactionFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +createFuelTank.Fragment(transactionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendCreateFuelTank(createFuelTank); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +// Coming Soon! +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation CreateFuelTank( + $name: String! + $coveragePolicy: CoveragePolicy + $reserveAccountCreationDeposit: Boolean + $whitelistedPallets: [String!] + $whitelistedCollections: [BigInt!] + ) { + CreateFuelTank( + name: $name + coveragePolicy: $coveragePolicy + reservesAccountCreationDeposit: $reserveAccountCreationDeposit + dispatchRules: [ + { + whitelistedPallets: $whitelistedPallets + whitelistedCollections: $whitelistedCollections + } + ] + ) { + id + method + state + } + } + `, + variables: { + name: "Collection Token Actions", //Specify the Fuel Tank name + coveragePolicy: "FEES_AND_DEPOSIT", //This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). + reserveAccountCreationDeposit: true, //This is set to true since we want to allow tank account creations, and we want the tank to subsidize the ENJ required for therequired for the Tank User Account storage deposit + whitelistedPallets: ["MultiTokens", "Marketplace"], //This rule specifies that only extrinsics from the MultiTokens and Marketplace pallets are subsidized + whitelistedCollections: [36105] //This rule ensures that only the specified collection is subsidized + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation CreateFuelTank( + $name: String! + $coveragePolicy: CoveragePolicy + $reserveAccountCreationDeposit: Boolean + $whitelistedPallets: [String!] + $whitelistedCollections: [BigInt!] + ) { + CreateFuelTank( + name: $name + coveragePolicy: $coveragePolicy + reservesAccountCreationDeposit: $reserveAccountCreationDeposit + dispatchRules: [ + { + whitelistedPallets: $whitelistedPallets + whitelistedCollections: $whitelistedCollections + } + ] + ) { + id + method + state + } + } + `, + variables: { + name: "Collection Token Actions", //Specify the Fuel Tank name + coveragePolicy: "FEES_AND_DEPOSIT", //This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). + reserveAccountCreationDeposit: true, //This is set to true since we want to allow tank account creations, and we want the tank to subsidize the ENJ required for therequired for the Tank User Account storage deposit + whitelistedPallets: ["MultiTokens", "Marketplace"], //This rule specifies that only extrinsics from the MultiTokens and Marketplace pallets are subsidized + whitelistedCollections: [36105] //This rule ensures that only the specified collection is subsidized + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation CreateFuelTank( + $name: String! + $coveragePolicy: CoveragePolicy + $reserveAccountCreationDeposit: Boolean + $whitelistedPallets: [String!] + $whitelistedCollections: [BigInt!] +) { + CreateFuelTank( + name: $name + coveragePolicy: $coveragePolicy + reservesAccountCreationDeposit: $reserveAccountCreationDeposit + dispatchRules: [ + { + whitelistedPallets: $whitelistedPallets + whitelistedCollections: $whitelistedCollections + } + ] + ) { + id + method + state + } +} +''' + +variables = { + 'name': "Collection Token Actions", #Specify the Fuel Tank name + 'coveragePolicy': "FEES_AND_DEPOSIT", #This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). + 'reserveAccountCreationDeposit': True, #This is set to true since we want to allow tank account creations, and we want the tank to subsidize the ENJ required for therequired for the Tank User Account storage deposit + 'whitelistedPallets': ["MultiTokens", "Marketplace"], #This rule specifies that only extrinsics from the MultiTokens and Marketplace pallets are subsidized + 'whitelistedCollections': [36105] #This rule ensures that only the specified collection is subsidized +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +### Subsidize All Transactions For Whitelisted Accounts With Budget Limitations + +The following mutation will set up a fuel tank that subsidizes any transaction, the tank is allowed to subsidize up to 5 ENJ per 30 days for each User Account in the tank, which can be created only if the account is within the whitelisted callers list. + +:::warning Upcoming Feature Notice: `requireAccount` Option +The `requireAccount` argument is not yet supported on the Enjin Platform. +The following code snippet is provided for illustrative purposes only. +If you wish to create a fuel tank with this option now, please use the Enjin Console at [console.enjin.io](https://console.enjin.io). +We will update the documentation once this option is available. +::: + + + +```graphql +mutation CreateFuelTank{ + CreateFuelTank( + name: "Only Specific Accounts Allowed" #Specify the Fuel Tank name + coveragePolicy: FEES_AND_DEPOSIT #This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it. + accountRules: { + whitelistedCallers: [ #This will validate that the caller is whitelisted at the time of adding the caller's account to the tank's User Accounts. + "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", + "cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP" + ] + } + dispatchRules: [{ + requireAccount: true #This is set to true since we want the tank to allow dispatching only if the caller has a User Account in the tank. + userFuelBudget: { #In here we configure how much ENJ to subsidize for each User Account. + amount: 5000000000000000000 #This will allow the tank to subsidize up to 5 ENJ from the Tank's pool per period for each User Account. + resetPeriod: 432000 #This sets the period to 432,000 blocks, which is 30 days on average with 6 seconds average block time. Meaning the tank will be able to subsidize up to 5 ENJ per 30 days for each User Account. + } + }] + ){ + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation CreateFuelTank(\r\n $name: String!\r\n $coveragePolicy: CoveragePolicy\r\n $whitelistedCallers: [String!]\r\n $requireAccount: Boolean\r\n $userFuelBudget: FuelBudgetInputType\r\n) {\r\n CreateFuelTank(\r\n name: $name\r\n coveragePolicy: $coveragePolicy\r\n accountRules: { whitelistedCallers: $whitelistedCallers }\r\n dispatchRules: [\r\n { requireAccount: $requireAccount, userFuelBudget: $userFuelBudget }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"name":"Only Specific Accounts Allowed","coveragePolicy":"FEES","whitelistedCallers":["cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu","cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP"],"requireAccount":true,"userFuelBudget":{"amount":5000000000000000000,"resetPeriod":432000}}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; +using Enjin.Platform.Sdk.FuelTanks; + +// Create an array of whitelisted callers and assign to accountRules.whitelistedCallers +var whitelistedCallers = new List() +{ + "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", + "cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP" +}; +var accountRules = new AccountRuleInputType() + .SetWhitelistedCallers(whitelistedCallers.ToArray()); + +// Create the user fuel budget input type +var userFuelBudget = new FuelBudgetInputType() + .SetAmount(5000000000000000000) // Set the amount of ENJ that the fuel tank will provide to users. + .SetResetPeriod(432000); // Set the reset period to 432000 blocks (approximately 30 days). + +// Create the array of dispatch rules for the fuel tank +var dispatchRules = new List() +{ + new DispatchRuleInputType() + .SetRequireAccount(true) // Note: This property has yet to be implemented on the platform. This is set to true since we want the tank to allow dispatching only if the caller has a User Account in the tank. + .SetUserFuelBudget(userFuelBudget) // Set the user fuel budget for the dispatch rule. +}; + +// Set up the mutation +var createFuelTank = new CreateFuelTank() + .SetName("Only Specific Accounts Allowed") // Set the name of the fuel tank. + .SetCoveragePolicy(CoveragePolicy.FeesAndDeposit) // This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). + .SetAccountRules(accountRules) // Set the account rules for the fuel tank. + .SetDispatchRules(dispatchRules.ToArray()); + +// Define and assign the return data fragment to the mutation +var transactionFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +createFuelTank.Fragment(transactionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendCreateFuelTank(createFuelTank); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +// Coming Soon! +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation CreateFuelTank( + $name: String! + $coveragePolicy: CoveragePolicy + $whitelistedCallers: [String!] + $requireAccount: Boolean + $userFuelBudget: FuelBudgetInputType + ) { + CreateFuelTank( + name: $name + coveragePolicy: $coveragePolicy + accountRules: { whitelistedCallers: $whitelistedCallers } + dispatchRules: [ + { requireAccount: $requireAccount, userFuelBudget: $userFuelBudget } + ] + ) { + id + method + state + } + } + `, + variables: { + name: "Only Specific Accounts Allowed", //Specify the Fuel Tank name + coveragePolicy: "FEES", //This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it. + whitelistedCallers: [ //This will validate that the caller is whitelisted at the time of adding the caller's account to the tank's User Accounts. + "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", + "cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP" + ], + requireAccount: true, //This is set to true since we want the tank to allow dispatching only if the caller has a User Account in the tank. + userFuelBudget: { //In here we configure how much ENJ to subsidize for each User Account. + amount: 5000000000000000000, //This will allow the tank to subsidize up to 5 ENJ from the Tank's pool per period for each User Account. + resetPeriod: 432000 //This sets the period to 432,000 blocks, which is 30 days on average with 6 seconds average block time. Meaning the tank will be able to subsidize up to 5 ENJ per 30 days for each User Account. + } + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation CreateFuelTank( + $name: String! + $coveragePolicy: CoveragePolicy + $whitelistedCallers: [String!] + $requireAccount: Boolean + $userFuelBudget: FuelBudgetInputType + ) { + CreateFuelTank( + name: $name + coveragePolicy: $coveragePolicy + accountRules: { whitelistedCallers: $whitelistedCallers } + dispatchRules: [ + { requireAccount: $requireAccount, userFuelBudget: $userFuelBudget } + ] + ) { + id + method + state + } + } + `, + variables: { + name: "Only Specific Accounts Allowed", //Specify the Fuel Tank name + coveragePolicy: "FEES", //This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it. + whitelistedCallers: [ //This will validate that the caller is whitelisted at the time of adding the caller's account to the tank's User Accounts. + "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", + "cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP" + ], + requireAccount: true, //This is set to true since we want the tank to allow dispatching only if the caller has a User Account in the tank. + userFuelBudget: { //In here we configure how much ENJ to subsidize for each User Account. + amount: 5000000000000000000, //This will allow the tank to subsidize up to 5 ENJ from the Tank's pool per period for each User Account. + resetPeriod: 432000 //This sets the period to 432,000 blocks, which is 30 days on average with 6 seconds average block time. Meaning the tank will be able to subsidize up to 5 ENJ per 30 days for each User Account. + } + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation CreateFuelTank( + $name: String! + $coveragePolicy: CoveragePolicy + $whitelistedCallers: [String!] + $requireAccount: Boolean + $userFuelBudget: FuelBudgetInputType +) { + CreateFuelTank( + name: $name + coveragePolicy: $coveragePolicy + accountRules: { whitelistedCallers: $whitelistedCallers } + dispatchRules: [ + { requireAccount: $requireAccount, userFuelBudget: $userFuelBudget } + ] + ) { + id + method + state + } +} +''' + +variables = { + 'name': "Only Specific Accounts Allowed", #Specify the Fuel Tank name + 'coveragePolicy': "FEES", #This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it. + 'whitelistedCallers': [ #This will validate that the caller is whitelisted at the time of adding the caller's account to the tank's User Accounts. + "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", + "cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP" + ], + 'requireAccount': True, #This is set to true since we want the tank to allow dispatching only if the caller has a User Account in the tank. + 'userFuelBudget': { #In here we configure how much ENJ to subsidize for each User Account. + 'amount': 5000000000000000000, #This will allow the tank to subsidize up to 5 ENJ from the Tank's pool per period for each User Account. + 'resetPeriod': 432000 #This sets the period to 432,000 blocks, which is 30 days on average with 6 seconds average block time. Meaning the tank will be able to subsidize up to 5 ENJ per 30 days for each User Account. + } +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +A WebSocket event will also be fired so you can pick up the Fuel Tank creation in real time by listening to the app channel on the WebSocket. + +:::info Explore More Arguments +For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. +::: + +## Dispatching a Call Using a Fuel Tank + +To broadcast a transaction call using a fuel tank, use the `Dispatch`/`DispatchAndTouch` mutation. + +:::tip The Fuel Tank requires a UserAccount to dispatch? +Use the `DispatchAndTouch` mutation to create a `UserAccount` and `Dispatch` at the same time in a single transaction. +::: + +:::tip Not sure which Fuel Tank to select? +If you need help figuring out the best fuel tank to use for a transaction, check out this page: [Selecting a fuel tank to dispatch with](/04-enjin-blockchain/03-enjin-matrixchain/02-fuel-tank-pallet.md#selecting-a-fuel-tank-to-dispatch-with). +::: + +### Step #1: Prepare The Mutation + +First, prepare the mutation you wish to dispatch, with the `id` and `encodedData` fields in the response. +In this example, we'll dispatch a call to send a from one account to another: + +:::danger Ask for the `id` and `encodedData` fields! +Make sure to add the `id` and `encodedData` fields in the response, or the mutation will fail with an error. +::: + +```graphql +mutation TransferNFT{ + SimpleTransferToken( + collectionId: 3298 + recipient: "efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr" + params: {tokenId: {integer: 1} amount: 1} + ){ + id + encodedData + } +} +``` + +### Step #2: Prepare The Dispatch Mutation + +Next, you need to convert the mutation call into a String. Remove all new lines and escape double quotation marks: + +``` +mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}} +``` + +To escape quotation marks you can use online tools such as https://tools.knowledgewalls.com/online-escape-single-or-double-quotes-from-string. + +### Step #3: Send the Dispatch Call + +Insert the mutation String from Step #2 into a Dispatch mutation call "query" parameter and send it: + + + +```graphql +mutation Dispatch { + Dispatch( + tankId: "efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou" #Specify the Fuel Tank ID to dispatch with + ruleSetId: 0 #Specify the ruleset to dispatch with + dispatch: { + call: MULTI_TOKENS #Specify the pallet to use for the transaction + query: "mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}" #Insert the mutation to dispatch. + variables: null #If the mutation has variables, insert them here. + } + ){ + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation Dispatch(\r\n $tankId: String!\r\n $ruleSetId: BigInt!\r\n $call: DispatchCall!\r\n $query: String!\r\n $variables: Object\r\n) {\r\n Dispatch(\r\n tankId: $tankId\r\n ruleSetId: $ruleSetId\r\n dispatch: { call: $call, query: $query, variables: $variables }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"tankId":"efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou","ruleSetId":0,"call":"MULTI_TOKENS","query":"mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}","variables":null}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; +using Enjin.Platform.Sdk.FuelTanks; + +// Create an array of whitelisted callers and assign to accountRules.whitelistedCallers +var dispatchInput = new DispatchInputType() + .SetCall(DispatchCall.MultiTokens) + .SetQuery("mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \\\"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\\\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}") // Insert the mutation to dispatch. + .SetVariables(null); + +// Set up the mutation +var dispatch = new Dispatch() + .SetTankId("efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou") + .SetRuleSetId(0) + .SetDispatch(dispatchInput); + +// Define and assign the return data fragment to the mutation +var transactionFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +dispatch.Fragment(transactionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendDispatch(dispatch); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +// Coming Soon! +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation Dispatch( + $tankId: String! + $ruleSetId: BigInt! + $call: DispatchCall! + $query: String! + $variables: Object + ) { + Dispatch( + tankId: $tankId + ruleSetId: $ruleSetId + dispatch: { call: $call, query: $query, variables: $variables } + ) { + id + method + state + } + } + `, + variables: { + tankId: "efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou", //Specify the Fuel Tank ID to dispatch with + ruleSetId: 0, //Specify the ruleset to dispatch with + call: "MULTI_TOKENS", //Specify the pallet to use for the transaction + query: "mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}", //Insert the mutation to dispatch. + variables: null //If the mutation has variables, insert them here as an object. + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation Dispatch( + $tankId: String! + $ruleSetId: BigInt! + $call: DispatchCall! + $query: String! + $variables: Object + ) { + Dispatch( + tankId: $tankId + ruleSetId: $ruleSetId + dispatch: { call: $call, query: $query, variables: $variables } + ) { + id + method + state + } + } + `, + variables: { + tankId: "efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou", //Specify the Fuel Tank ID to dispatch with + ruleSetId: 0, //Specify the ruleset to dispatch with + call: "MULTI_TOKENS", //Specify the pallet to use for the transaction + query: "mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}", //Insert the mutation to dispatch. + variables: null //If the mutation has variables, insert them here as an object. + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation Dispatch( + $tankId: String! + $ruleSetId: BigInt! + $call: DispatchCall! + $query: String! + $variables: Object +) { + Dispatch( + tankId: $tankId + ruleSetId: $ruleSetId + dispatch: { call: $call, query: $query, variables: $variables } + ) { + id + method + state + } +} +''' + +variables = { + 'tankId': "efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou", #Specify the Fuel Tank ID to dispatch with + 'ruleSetId': 0, #Specify the ruleset to dispatch with + 'call': "MULTI_TOKENS", #Specify the pallet to use for the transaction + 'query': "mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}", #Insert the mutation to dispatch. + 'variables': None #If the mutation has variables, insert them here as an object. +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +Once the mutation is sent, signed and broadcasted, If the transaction is eligible, the fuel tank will subsidize the transaction fees; otherwise, the transaction will fail. + +:::info Need to broadcast from Managed Wallet? +To broadcast the transaction from a managed wallet account, add the `signingAccount` argument as instructed in the [Using Managed Wallets](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md#transferring-tokens-from-managed-wallets) page. +It's important to note that the `signingAccount` argument should be added on the `Dispatch` mutation level, and **NOT** in the dispatch argument. +In the above example, to transfer the token from a managed wallet via a fuel tank: + +```graphql +mutation Dispatch { + Dispatch( + tankId: "efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou" #Specify the Fuel Tank ID to dispatch with + ruleSetId: 0 #Specify the ruleset to dispatch with + signingAccount: "Managed Wallet Address Here" + dispatch: { + call: MULTI_TOKENS #Specify the pallet to use for the transaction + query: "mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}" #Insert the mutation to dispatch. + variables: null #If the mutation has variables, insert them here. + } + ){ + id + method + state + } +} +``` +::: \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/_category_.json new file mode 100644 index 0000000..e434e3c --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Managing Users" +} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md new file mode 100644 index 0000000..1b01b7d --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md @@ -0,0 +1,188 @@ +--- +title: "TokenID Structure" +slug: "tokenid-structure" +description: "Learn about the Enjin TokenID structure, its format, and how to utilize it effectively to create and manage blockchain-based assets in your projects." +--- +# Token ID Structure + +Token IDs in a collection serve as unique identifiers for tokens, represented as 128-bit integers. Beyond their primary role, token IDs can also store structured information about a token's attributes, providing greater organizational flexibility. + +This guide outlines recommended approaches for structuring and organizing token IDs, as well as the four encoding options available on the Enjin Platform: **ERC1155**, **Hash**, **StringId**, and **Integer** (no encoding). + +*** + +## Token ID Structuring Approaches + +There are 4 key methods for Token ID organization: + +1. [Bitmasks](#1-bitmasks) +2. [Ranges](#2-ranges) +3. [Hashes](#3-hashes) +4. [Integer (No Encoding)](#4-integer-no-encoding) + +*** + +### 1. **Bitmasks** + +Bitmasks allocate specific bits of the 128-bit token ID to various attributes, such as **game**, **server**, **class**, and **item ID**. Each section of the token ID is defined by the bit size required for that attribute, ensuring efficient use of space. + +For example: + +- **Game ID**: 4 bits (up to 16 games) +- **Server ID**: 8 bits (up to 256 servers) +- **Class ID**: 16 bits (up to 65,536 classes) +- **Item ID**: Remaining 100 bits + +If you create a token with the ID `0x05ffa34f000000000000000000000001`, it can be decoded as: + +- **Game ID**: `05` (Game 5) +- **Server ID**: `ff` (Server 255) +- **Class ID**: `a34f` (Sword) +- **Item ID**: `1` + +This method is compact but requires encoding and decoding logic. + +*** + +### 2. **Ranges** + +Ranges define numeric intervals for each category, making the structure simpler to understand and decode. + +for example: + +- **Game 01**: Token IDs 0–1000 + - **Server 01**: 0–300 + - **Class A**: 0–20 + - **Class B**: 20–40 + - **Server 02**: 300–600 + +Token ID `325` maps to: + +- **Game ID**: `01` +- **Server ID**: `02` +- **Class ID**: `B` +- **Item ID**: `5` + +This approach is intuitive but may require manual configuration of ID ranges. + +*** + +### 3. **Hashes** + +Hashes use cryptographic functions like **SHA256**, **Keccak256**, or **Blake2** to generate unique token IDs. The input can include attributes such as game name, server, or class. + +for example: + +``` +{ + token_id: hash(hash("Game01") || hash("Server01") || hash("Sword")) +} +``` + +This guarantees uniqueness but makes decoding impossible, as hashing is a one-way process. + +*** + +### 4. **Integer (No Encoding)** + +Using Integer IDs involves assigning unique and raw integers without additional encoding. This is the simplest method but does not include embedded metadata. + +for example: + +Token IDs are manually assigned, such as `1`, `2`, `3`, etc. This method is straightforward but lacks flexibility for organization. + +*** + +## Enjin Platform Token ID Encoders + +The Enjin Platform provides built-in encoders to convert data into 128-bit token IDs: + +### 1. **ERC1155 Encoder** + +This encoder uses a 16-bit hexadecimal token ID and a 64-bit integer index. + +```graphql +tokenId: { + erc1155: { + tokenId: "0x78c0000000003377", index: 10 + } +} +``` + +Resulting Token ID: `160504280491028834688987873652194148362` + +*** + +### 2. **Hash Encoder** + +This encoder generates a token ID from a hashed array or object. + +for example: + +```graphql +tokenId: { + hash: { + Token: "Sword", Number: 5 + } +} +``` + +Resulting Token ID: `65304702016350863193892557847492631289` + +Token IDs generated with this method cannot be reversed or decoded. + +:::info Irreversible Token IDs with the Hash Encoder +Token IDs generated using the Hash Encoder are derived from a cryptographic hash function. As hashing is a one-way process, these token IDs cannot be reversed or decoded back into the original data. +::: + +:::danger Important Note +The Hash Encoder generates token IDs by hashing input data. If the resulting hash exceeds 128 bits, the encoder will fail. +::: + +*** + +### 3. **StringId Encoder** + +This encoder converts a string into a numeric token ID. + +for example: + +```graphql +tokenId: { + stringId: "MyToken-0001" +} +``` + +Resulting Token ID: `23977024514528806328972881969` + +:::danger Important Note +The StringId Encoder converts strings into numeric token IDs. Strings that produce integers larger than 128 bits will cause the encoding to fail. +::: + +*** + +### 4. **Integer** + +For direct assignment, integers can be passed as-is to create token IDs. + +for example: + +```graphql +tokenId: {integer: 1001} +``` + +Resulting Token ID: `1001` + +This approach is simple and requires no encoding or decoding. + +*** + +## Choosing the Right Approach + +### Considerations: + +- **Ease of Use**: Use Integers or ranges for simplicity. +- **Scalability**: Bitmasks and hashes work best for large collections with many attributes. +- **Interoperability**: Use the ERC1155 encoder for compatibility with Ethereum-based assets. + +By understanding these methods and tools, you can design token ID structures that best suit your project's needs while ensuring flexibility and scalability. diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-example.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-example.md new file mode 100644 index 0000000..7f334f0 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-example.md @@ -0,0 +1,104 @@ +--- +title: "Metadata Example" +slug: "metadata-example" +description: "View detailed examples of how to implement metadata within your blockchain tokens, following Enjin’s guidelines for token structure and data representation." +--- +In this page we will create JSON formatted metadata for a collection and a token, following the [Metadata Standard](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md). + +You may set the collection/token `uri` on-chain attribute to a resource containing the collection's/token's metadata in JSON format, or set each individual metadata with an on-chain attribute, using the metadata type as the attribute key. +For example, to set the token's description individually, set the token's on-chain attribute key to `description`, and provide the description as the attribute's value. +If both on-chain and off-chain attributes are set, the on-chain attributes will take precedence over the off-chain attributes. + +:::warning **Important:** Attribute keys are case sensitive. +Ensure you use the correct casing when defining attributes to avoid errors. +::: + +## Collection Metadata + +```json +{ + "name": "The Multiverse", + "description": "A blockchain gaming multiverse is a collective gaming reality created by integrating a collection of blockchain assets with multiple games. In other words, a gaming multiverse is a collaborative gaming project where multiple game developers agree to use the same shared, decentralized database for some (or even all) of their in-game assets.\nThis enables gamers to utilize a multiverse asset in every game that is a part of a specific gaming multiverse (e.g., if a player owns a sword in Game A, they will also own it and can use it in Game B). In-game assets in a blockchain gaming multiverse are owned by gamers, while individual game developers control only the games they create.\nThe Enjin Multiverse is the first blockchain gaming multiverse ever.\nIt was announced on August 26, 2018, when six games (9Lives Arena, Age of Rust, Bitcoin Hodler, CryptoFights, Forest Knight, and War of Crypta) decided to collaborate and implement the first shared blockchain assets.\nIt has since grown to over 30 games.", + "media": [ + { + "url": "https://cdn.enjinx.io/assets/images/ethereum/platform/0/apps/8/2b728df41fadef568e4410fb823999d14473ef1e.jpeg", + "type": "image/jpeg" + }, + { + "url": "https://cdn.enjin.io/mint/image/15.jpg", + "type": "image/jpg" + }, + + ], + "banner_image": "https://platform.production.enjinusercontent.com/enterprise/enjin/assets/media/2024-multiverse.banner.jpg", + "fallback_image": "https://cdn.enjin.io/mint/image/15.jpg", + "external_url": "https://enjin.io/multiverse" +} +``` + +### Attribute Keys Explained: + +- `name`: The collection's display name. +- `description`: The collection's description. +- `media`: A list of links to media files (images, GIFs, videos, etc.) to be displayed as the collection media. + - The NFT.io marketplace uses the first provided media file as the collection's featured media. +- `banner_image`: A link to an image file to be displayed as the collection banner. (Recommended ratio: 4:1) +- `fallback_image`: A link to an alternative image file to be used when the media provided in the `media` list fails to load. +- `external_url`: A link to the collection's website. + +## Token Metadata + +```json +{ + "name": "Primythical Chest", + "description": "As written in the fourth edition of 'Book of the Ruindawn', Primythical Chests contain fabled treasures, and are hidden through a myriad of universes by the Creators of Realms themselves—the Architects.\nThe wood-and-iron vaults are shrouded by the veils of time and space—and can only be found by the most courageous and intelligent Wanderers.", + "media": [ + { + "url": "https://cdn.enjin.io/mint/images/50000000000000c3.jpg", + "type": "image/jpg" + }, + { + "url": "https://cdn.enjin.io/mint/videos/chest.mp4", + "type": "video/mp4", + }, + { + "url": "https://cdn.enjin.io/mint/models/chest.glb", + "type": "model/gltf-binary", + } + ], + "fallback_image": "https://cdn.enjin.io/mint/images/50000000000000c3.jpg", + "attributes": { + "Usable In": { + "value": "Lost Relics, Etherscape, Axiomech" + }, + "Artifact Rarity": { + "value": "Common" + } + }, + "external_url": "https://enjin.io/multiverse", + "keywords": [ + "Multiverse Item", + "Key", + "Free" + ] +} +``` + +### Attribute Keys Explained: + +- `name`: The token's display name. +- `description`: The token's description. +- `media`: A list of links to media files (images, GIFs, mp4, GLB, etc.) to be displayed as the token media. + - On the Enjin Wallet and NFT.io marketplace, when more than one media file is provided, the media is scrollable. +- `fallback_image`: A link to an alternative image file that serves as a backup when the media files listed under `media` fail to load or are unsupported in certain views. + - Best Practice: Use an image hosted on a different server to ensure availability in case of server downtime. + - The `fallback_image` is also helpful for views that do not support the primary media file. + Example: If the main media file is a 3D object (e.g., GLB), a fallback image can be displayed in views that lack 3D rendering support. +- `attributes`: A list of custom properties attached to the token. In the above example, it's used as an indicator of the item's common rarity and a list of games it's usable in. +- `external_url`: A link to the token's website or a website with information on the token. +- `keywords`: A list of keywords relevant to this token. Used to help find this token in search queries. + - In the NFT.io Marketplace, keywords also act as tags. + +:::info Need more information? +For a comprehensive document on the metadata structure, please head over to the [Universal Off-Chain Token Metadata Standard](https://github.com/enjin/universal-metadata-standard/blob/uotm-standard-wip/README.md) page. +::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md new file mode 100644 index 0000000..b40d571 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md @@ -0,0 +1,79 @@ +--- +title: "Metadata Standard" +slug: "../metadata-standard" +description: "Explore Enjin’s metadata standards, enabling you to define the characteristics of your blockchain tokens and enhance their utility within decentralized applications." +--- +## Best Practices for Collection/Token Metadata + +Metadata plays a vital role in the token ecosystem. It provides necessary details about a token and ensures that tokens are presented correctly across various applications. +To maintain consistency and ensure universal compatibility, it's important to adhere to a specific standard for metadata. + +### Why Follow a Metadata Standard? + +Following a standard for metadata ensures that token details are accurately represented across all applications that support the standard. This eliminates inconsistencies and ensures that your tokens are presented in the way you intended. + +Adhering to a standard also simplifies the process for application developers, as they can easily support tokens that follow the standard. This contributes to the goal of creating a truly interoperable Metaverse. + +For more details on the standard, refer to our [Universal Off-Chain Token Metadata Standard](https://enj.in/metadata). + +### Creating Metadata + +:::info Need an example? +For a complete example of Collection & Token metadata, check out the [Metadata Example page](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-example.md) +::: + +:::warning **Important:** Attribute keys are case sensitive. +Ensure you use the correct casing when defining attributes to avoid errors. +::: + +There are two ways to set metadata for your tokens: + +1. **Off-chain - Hosted JSON File:** Set the collection or token attribute with the key `uri` and the value as a URL to a off-chain hosted JSON file. This file can be hosted either in a centralized manner or on IPFS. + The JSON file should contain metadata that follows the structure defined in the metadata standard. + Example: + ```graphql + mutation SetMetadataUsingJSON { + BatchSetAttribute( + collectionId: 36105 # Specify the collection ID + tokenId: {integer: 0 } # Specify the token ID + attributes: [ + { + key: "uri" + value: "https://yourhost/metadata.json" #Link to your hosted JSON file. + } + ] + ) { + id + method + state + } + } + ``` + +2. **On-chain - Directly Setting Metadata:** Set the metadata directly on-chain using a structure that follows the metadata standard. + Example: + ```graphql + mutation SetMetadataDirectly { + BatchSetAttribute( + collectionId: 36105 # Specify the collection ID + tokenId: {integer: 0 } # Specify the token ID + attributes: [ + { + key: "name" #This attribute key defines the token name, following the metadata standard. + value: "Chronicles of the Celestium" #Token name + }, + { + key: "description" #This attribute key defines the token description, following the metadata standard. + value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." #Token description + } + ] + ) { + id + method + state + } + } + ``` + +Whichever method you choose, it's crucial to ensure the metadata is structured according to the standard to maintain compatibility and present your tokens correctly. +For a detailed guide on adding metadata, please refer to our [Adding Metadata Tutorial](/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md). \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md new file mode 100644 index 0000000..0e7a7cb --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md @@ -0,0 +1,188 @@ +--- +title: "Dynamic Metadata" +slug: "dynamic-metadata" +description: "Learn to use Dynamic Metadata to efficiently manage large NFT collections. Our easy-to-follow guide shows how to set one URL for all your tokens, saving time and on-chain costs." +--- + +What if you have a collection with 10,000 unique tokens? Setting up metadata (like the name, image, and description) for each one sounds like a lot of work, and it can be costly. + +**Dynamic metadata** is our simple and efficient solution for this. + +Instead of putting metadata on *every single token*, you just set **one link** on your entire **collection**. Applications like the Enjin Wallet and NFT.io marketplace will then use that single link to automatically find the correct metadata for each specific token as it's needed. + +### 🎯 Why Use Dynamic Metadata? + + * **Save Time & Effort:** Manage metadata for thousands (or millions\!) of tokens from one central place. + * **Save on Costs:** This method uses much less on-chain data, which can reduce the ENJ required to create your tokens. + * **Easy to Update:** Want to change your token's image? Just replace the media file on your server. There's no need for a new on-chain transaction. + +----- + +## How Dynamic Metadata Works + +Understanding this is key to setting it up correctly. + +### How Your Metadata is Found (The Hierarchy) + +When you look at an NFT in your wallet, the wallet needs to find its image and name. It searches in a specific order: + +1. **Token:** First, it checks for metadata *directly on that single token*. +2. **Group (Optional):** If not, it checks if the token is in a *group* that has metadata. +3. **Collection:** If it's still empty, it finally looks at the *whole collection* for the metadata link. + +Dynamic metadata works by setting the link at the **Collection level** (step 3). This way, all tokens in that collection will use this link as their default. + +### The Magic `{id}` Placeholder + +This is the most important part. The single link you set on your collection will include the special placeholder: `{id}.json` + +> **Example link:** `https://my-server.com/metadata/{id}.json` + +When a wallet needs to find a token's details, it automatically replaces `{id}` with the correct filename. Your job is to make sure your files are hosted and named correctly so the wallet can find them. + + +### Let's Walk Through an Example + +Seeing this in action makes it much clearer. + +Let's imagine you have a collection called **"Hero's Hoard"** which has an ID of **`8143`**. +Inside this collection, you have a token called **"Dragon's Fang"** with an ID of **`72`**. + +You have already set up dynamic metadata for your collection, and the `uri` attribute on your collection (`8143`) is: +`ipfs://bafy...xyz/{id}.json` + +Now, a user opens the NFT.io marketplace to look at your "Dragon's Fang" token. Here's what happens behind the scenes: + +1. **NFT.io needs metadata for token `72` in collection `8143`.** +2. It checks the token itself for a `uri`. **Nothing is set there.** +3. It checks the token's group (if any) for a `uri`. **Nothing is set there either.** +4. It checks the *collection* (`8143`) for a `uri`. **Success!** It finds your dynamic link: `ipfs://bafy...xyz/{id}.json`. +5. NFT.io knows it's looking for a *token*, so it uses the token file name convention: `-.json`. +6. It replaces the `{id}` in your link with **`8143-72`**. +7. The final URL it fetches is: `ipfs://bafy...xyz/8143-72.json`. + +Your server (or Pinata, in our example) then sends this specific file, which contains the name "Dragon's Fang" and its unique image. That's how it all connects! + +### What You Must Name Your Files + +For this to work, your hosted files **must** use these exact naming conventions: + + * **For your Collection's metadata:** `.json` + * *Example: `8143.json`* + * **For each Token (NFT) metadata:** `-.json` + * *Example: `8143-72.json` (This is for Token 72 in Collection 8143)* + * **For a Token Group metadata (Optional):** `-group.json` + * *Example: `5-group.json`* + +----- + +## How to Set Up Dynamic Metadata: A Step-by-Step Guide + +This setup is a three-step process. We'll walk you through it using a free, easy-to-use service called [Pinata.cloud](https://www.pinata.cloud/), which hosts files on a decentralized network called IPFS. + +:::tip Do I have to use IPFS? +You can use any public server or hosting service, but Pinata is a great choice for beginners. +::: + +### Step 1: Host Your Media Files (Images, Videos, etc.) + +First, let's get all your images and videos uploaded. + +1. Sign up for a free account at [Pinata.cloud](https://www.pinata.cloud/). +2. On your computer, create a folder and put all your media files into it (e.g., `collection-banner.png`, `token-1.jpg`, `token-2.mp4`). +3. On your Pinata dashboard, click the blue "**+ Add**" button in the top-right corner, then select **Folder Upload**. +4. Upload the folder you just prepared. +5. Once it's finished, you'll see your folder in the file list. Copy its **CID** (a long string of letters and numbers). +6. Save this CID somewhere safe (like a notepad). This is your ``. + + + +### Step 2: Create and Host Your JSON Metadata Files + +Next, we'll create simple text files (called JSON files) that tell the wallet *about* your tokens, including where to find the media files you just uploaded. + +1. **Create your Collection JSON file:** + + * On your computer, create a new text file and name it *exactly* `.json` (e.g., `8143.json`). + * Inside this file, add your collection's details like `name` and `description`, following the [Metadata Standard](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md). + * For the `media` or `image` field, add the URL to your collection's media file from Step 1. The format is: `ipfs:///filename.png` + * *Example:* `"url": "ipfs://bafy...abc/collection-banner.png"` + +2. **Create your Token JSON files:** + + * Now, do the same for *each* token. Create a new text file and name it *exactly* `-.json` (e.Example: `8143-72.json`). + * Inside, add that token's specific `name`, `description`, `attributes`, etc. + * For its `media`, point to its specific media file: `ipfs:///token-72.jpg` + * *Example:* `"url": "ipfs://bafy...abc/token-72.jpg"` + +3. **Create your Group JSON files (Optional):** + + * If you use Token Groups, repeat the process. Name the file `-group.json` (e.Example: `5-group.json`). + +4. **Upload Your JSON Folder:** + + * Once you've created all your `.json` files, put them *all* into a **new, separate folder** on your computer. + * Go back to Pinata and upload this new folder (the one containing all your `.json` files). + * When it's done, copy the **CID** for this **JSON folder**. This is the final piece you need! Let's call it your ``. + +### Step 3: Update Your Collection's On-Chain URI + +Finally, let's tell your collection where to find these new metadata files. You'll do this by setting its `uri` attribute. + +1. The `value` you need to set is your **``** from the last step, followed by the `/{id}.json` placeholder. + * **Example Value:** `ipfs://bafy...xyz/{id}.json` (Replace `bafy...xyz` with your actual ``). +2. Set this attribute using the `BatchSetAttribute` mutation. You can copy/paste the example below into the [graphiql playground](https://platform.enjin.io/graphiql), and change the values. + +```graphql +mutation SetDynamicMetadataURI { + BatchSetAttribute( + collectionId: 8143 # 👈 Change this to your Collection ID + # We're not setting a tokenId, so this applies to the whole collection + attributes: [ + { + key: "uri" + # 👇 Change this to your JSON folder's CID + the {id} placeholder + value: "ipfs://bafy...xyz/{id}.json" + } + ] + ) { + id + state + } +} +``` + +3. Run this mutation, and you're all set\! Wallets and marketplaces will now automatically find your metadata. + +:::warning **Important:** Do Not Replace `{id}` +You must use the literal string `{id}.json` at the end of your URI. **Do not** replace `{id}` with an actual ID. The wallet or marketplace handles this replacement for you. +::: + +:::info +For a more detailed guide on updating metadata using the Enjin Platform, check out [this page](/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md). +::: + +----- + +## Migrating Legacy Metadata (For collections from Ethereum/JumpNet) + +This section is **only** if you are moving a collection from Ethereum or JumpNet and *already* used dynamic metadata there. The token ID format has changed, so you just need to rename your old `.json` files. + +### Conversion Steps + +1. Find the new **Collection ID** for your collection on Enjin Blockchain (e.g., `2000`). +2. Create a new JSON file for your collection, named `.json` (e.g., `2000.json`). +3. Now, for each token, find its **old** `.json` filename. + * *Old Example:* `70800000000000af000000000000000000000000000000000000000000000005.json` +4. **Follow these steps to get the new name:** + 1. **Remove the 32 zeros** from the middle (from position 17 to 48). *Result:* `70800000000000af0000000000000005.json` + 2. **Remove the `.json`** at the end. *Result:* `70800000000000af0000000000000005` + 3. **Convert this number.** This is a "hexadecimal" number. Use any online "hex to decimal" converter to change it. *Result:* `149538149525803038929858507180710297605` + 4. **Add `.json` back** to the end. **New Filename:** `149538149525803038929858507180710297605.json` +5. Repeat this renaming process for all your old token JSON files. +6. Upload all your **newly renamed** files (plus your new collection file, e.g., `2000.json`) into a new folder on your server or IPFS. +7. Finally, update your collection's `uri` attribute (like in [Step 3 above](#step-3-update-your-collections-on-chain-uri)) to point to this new folder, making sure to add `{id}.json` at the end. + * *Example:* `httpss://example.com/metadata/{id}.json` \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/_category_.json new file mode 100644 index 0000000..6c388d8 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Metadata Standard" +} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/03-enforced-rarity.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/03-enforced-rarity.md new file mode 100644 index 0000000..7824cda --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/03-enforced-rarity.md @@ -0,0 +1,31 @@ +--- +title: "Enforced Rarity" +slug: "enforced-rarity" +description: "Learn how to enforce rarity in your blockchain assets using Enjin's features, ensuring controlled distribution and enhanced value of rare tokens." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +## NFT Collection + +Consider a situation where your goal is to create a limited to 1,000 tokens, where each token is an symbolizing a distinct in-game sword. + +Accomplishing this requires the right configuration of your Mint Policy: + +![Enforced NFT Collection](/img/guides/advanced-mechanics/enforce-nft-collection.png) + +By setting the "**Max Token Count**" to `1,000`, you can cap the total number of tokens in the swords collection to 1,000. + +Meanwhile, setting the "**Max Token Supply**" to `1` ensures that each token in the swords collection will have a maximum supply of `1`. This enforces the uniqueness of each token in the collection, enforcing each token in the collection to be an NFT. + +## Immutable Tokenomics + +Suppose you want to create a with one simple but crucial rule: Each token has a pre-defined max supply which can only be decreased. +Achieving this requires careful configuration of your Mint Policy: + +

+ Enforced Collapsable Supply +

+ +By toggling the "**Force Collapsing Supply**", you ensure that each token in the collection will be of collapsing supply type. +This guarantees the immutability of each token's maximum supply in the collection, maintaining its value and scarcity over time. diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/04-brand-collab-nfts.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/04-brand-collab-nfts.md new file mode 100644 index 0000000..75f6b49 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/04-brand-collab-nfts.md @@ -0,0 +1,218 @@ +--- +title: "Brand Collab NFTs" +slug: "brand-collab-nfts" +description: "Discover how to create brand collaboration NFTs, allowing for innovative partnerships that utilize blockchain to engage audiences with exclusive digital assets." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Co-Authored by [SamTheBay](https://twitter.com/SamTheBay) +Sam is the Founder of [Etherscape](https://www.etherscape.io/), a multiplayer RPG with some of the most innovative Web3 game design elements in the industry. + +[Reach out to Sam on Discord](https://discord.gg/NXwWehg89p) for partnership and collaboration opportunities. +You can see examples of Brand Integrations in [Etherscape's whitepaper](https://whitepaper.etherscape.io/CryptoIntegration/multiverse-support/collaborations). +::: + +As a developer, you can leverage brand integrations and multiverse items - NFTs that work across multiple games - to increase your playerbase and create new monetization methods. + +These NFTs are stored on a public ledger, enabling your game to identify items a player owns, even if they were created elsewhere. + +When a player with a brand or multiverse item enters your game, you can scan their wallet, recognize the item, and integrate it into your game's environment. This approach offers several benefits: + +- Attract players who possess specific items. +- Accumulate these items to draw players interested in obtaining them through your game. +- Get compensated for incorporating these items into your game. +- Consider charging a nominal fee for unlocking the item's features in your game. + +While the concept of multiverse items is appealing for players, it's important for you as a developer to adopt certain development and marketing strategies to ensure you reap measurable benefits from this integration. + +## Acquisition + +Your goal is to inform all existing and potential NFT owners that they can utilize their NFTs in your game, and to give them compelling reasons to engage with your game using these NFTs. + +For this initiative to be successful for you, it's important to implement a range of strategic marketing actions. + +### Show off Your Gameplay and Aesthetics + +Using videos and GIFs is an excellent method to showcase your game's gameplay and visual appeal. They effectively demonstrate the experience players can anticipate when they play your game and utilize the NFTs within it. + +### Create an Exciting Announcement + +Announce that your game has integrated new functionalities for these NFTs. In your announcement, include video footage displaying how the NFT is used in your game, and highlight the enjoyable aspects of your gameplay to engage your audience. + +It's crucial to communicate the reasons behind your decision to integrate these NFTs. Share what aspects of the NFT community you're supporting appeal to you and how your values and vision align with theirs. + +### Ongoing Content Pushes + +Regularly create and share posts, videos, and GIFs that highlight the versatility and capabilities of the NFT, usable across multiple games. This approach not only emphasizes the expanding usefulness of the NFT but also continuously promotes the games that support it. + +### List Your Game on the Token + +Ensure your game is included as a utility provider in the token's metadata. This step guarantees that your game can be found through the token's presence on various wallets and marketplaces. + +### Utility Unlock Challenge + +By designating your game's utility as "locked" on the token and clarifying that players can unlock this utility by engaging with your game, you provide them with a direct incentive to play your game. This strategy motivates players to explore your game in order to access the additional utility of their NFT. + +## Monetization + +To monetize collaborations effectively, it's important to create strategies that are enjoyable for players and advantageous for both the NFT creator and the utility provider. + +### Commissioned Integrations + +The NFT creator can opt to pay a flat fee to cover the costs of integrating their NFT into your platform, including the development of engaging and unique functionalities for the NFT. + +### Community-Commissioned Integrations + +You have the option to set up a community fundraising campaign on platforms like GoFundMe, where a specific funding goal is established. If this goal is met, you'll integrate the item in question. + +The holders of the NFT will likely be inclined to contribute to this fund, as reaching the target means adding additional functionalities to their token. + +### Utility Unlock Fees + +By marking the utility of your game on the token’s metadata as "locked," and clarifying that players can unlock it by paying a small fee, you provide a straightforward incentive for them to pay and access the additional value and functionalities of their NFT in your game. + +## Development + +Here’s the Query you will need to use: + + + +```graphql +query GetWallet { + GetWallet(account:"efUVQhs3Q1ih9RKn1ukpb45p2Zomu5jvopsQE6n5LtW52FJH3") { +// omit “tokenIds” to read all tokens in a collection +// if you are getting a page after the first put the “endCursor” value into the “after” field +tokenAccounts (collectionIds:2100 tokenIds:1 after:"" first:100) { + pageInfo { + hasNextPage + endCursor + } + edges{ + node { + balance + reservedBalance + token{ + tokenId + attributes { + key + value + } + } + } + } + } + } +} + +``` + + +``` +curl -X POST "https://platform.canary.enjin.io/graphql" \ +-H "Content-Type: application/json" \ +-H "Authorization: enjin_api_key" \ +-d '{ + "query": "query GetWallet { GetWallet(account: \"efUVQhs3Q1ih9RKn1ukpb45p2Zomu5jvopsQE6n5LtW52FJH3\") { tokenAccounts(collectionIds: 2100, tokenIds: 1, after: \"\", first: 100) { pageInfo { hasNextPage endCursor } edges { node { balance reservedBalance token { tokenId attributes { key value } } } } } } }" +}' +``` + + + +Here’s an example of a response you can expect. + +```json +{ + "data": { + "GetWallet": { + "tokenAccounts": { + // if there are more items to read there will be a guid in the “endCursor” value + "pageInfo": { + "hasNextPage": false, + "endCursor": "" + }, + "edges": [ + { + "node": { + "balance": "3", + "reservedBalance": "0", + "token": { + "tokenId": "1", + // to read the metadata I should query the url provided as the value for the uri attribute + "attributes": [ + { + "key": "uri", + "value": "https://etherscapegame.com/crypto/ImperialSovereign.json" + } + ] + } + } + } + ] + } + } + } +} + +``` + +## Best Practices + +:::tip Tips + +- Only pull data from the collections you need. This will improve performance and ensure your backend infrastructure remains scalable. +- Always read and check the balance. Sometimes, the player may hold the token but the balance is actually zero, for instance, if the token has been burned. These burned tokens should not have in-game utility. +- Consider whether you want NFTs that are listed on a marketplace to have utility in your game. Items that are listed for sale don't show up in regular token balance and show up in reserved balance instead. If you want them to have utility, you can check the reserved balance and include the reserved supply. +- Consider if you need to use pagination. Users can have hundreds of tokens, in this situation you will need to read them in multiple calls. +- If you plan to utilize metadata from on-chain or external sources, it's important to also read the token's attributes. Typically, you'll find a "uri" attribute that points to the external location of this metadata. +- When accessing external metadata or media, make sure to do so asynchronously and think about storing it in a local cache for faster retrieval. +::: + +### Syncing Tokens + +In order to implement any token in game you will need to sync a list of token ids that your player has in their wallet. In the most simple case, you only need to sync tokens from a single collection (likely the main collection associated with your game). However, in multiverse cases, you will now need to sync tokens from multiple collections. There are a couple of options to achieve this. + +1. Sync All Tokens + The first approach is to sync all the tokens in the players wallet across all collections. This makes the graphQL query very simple. However, with this approach you need to bear in mind that some wallets will have thousands of tokens in them (and could grow to even hundreds of thousands). This means that you can’t rely on syncing all the wallets tokens in a single call. You will need to make sure to implement proper pagination using the GraphQL API’s so that you can read out the tokens in many batches. +2. Sync Specific Collections Only + Another approach is to only sync the collections you care about. This will limit the total number of tokens that you need to sync. In your GraphQL query you can specify a list of collection and token ids in order to narrow down the range. Depending on your case, you may still want to handle pagination of calls if any of the collections you include have a large number of tokens. + +### Syncing Metadata + +When implementing multiverse items you will want to think about how you will represent them in game. This generally means that you are going to want to provide metadata and media for them. Below are a few options for your consideration. + +1. Static Metadata + If you already know exactly what you want the token to do in your game and it won’t change then you can essentially hard code it. In this case, there is no need to read the metadata from the chain or from an external source. If the group and id match your intention then you can give it the in game visuals and utility that you want. +2. External Sources + The most common practice is to store metadata off-chain at an external url. This is especially true for any media content such as images, videos, 3d models and audio. If you want to leverage this metadata then you will need to make calls to the URI that hosts it in order to download it. When doing that, you will want to consider the following… + 1. Make sure the calls are asynchronous. The external calls will take time and you don’t want them to impact your games frame rate. + 2. You probably want to cache any media locally on the drive. The URI you are downloading from could be slow, unreliable or throttle you. As a result, you will want to avoid reading it more often than necessary. + +:::tip Metadata Management +Note that if you’re adopting another game’s token, you may not want to use all the metadata that is provided. For example, if you are making a 2D game you might want to create your own sprite to represent it instead of using a stylistic image that doesn’t suit your game. For example, this is how the ForgeHammer is represented in Etherscape - a 2D pixel-art game. +

+ Adjust the item's art style to fit your game +

+::: + +### ERC-1155 Grouped NFTs + +There are many tokens that have been migrated from the ERC-1155 version of Enjin. In ERC-1155 there is a concept called “Grouped NFTs” where there is a grouping of NFT tokens that share the same metadata. + +A popular example of this would be “Oindrasdain” from “The Multiverse” collection. There are many copies of Oindrasdain and they have token ids that look like the following… + +`107002853660685728488179487227240257220` + +If you convert this token id into hex, you will be able to see its 128 bit representation like the following… + +`508000000000002700000000000016C4` + +This can then be broken into two 64 bit parts (or 16 hex string characters) like the following… + +Group ID: `5080000000000027` + +Group Index: `00000000000016C4` + +All tokens in the collection that share the same group id are intended to be of the same token type and thus should get the same utility. This is similar to how s work. diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/05-enj-infusion.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/05-enj-infusion.md new file mode 100644 index 0000000..cabf186 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/05-enj-infusion.md @@ -0,0 +1,224 @@ +--- +title: "ENJ Infusion" +slug: "enj-infusion" +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Introduction to ENJ Infusion + +ENJ Infusion is an innovative feature of the Enjin Blockchain that allows users to lock Enjin Coin (ENJ) into tokens they create, whether they are s or s. This process effectively embeds ENJ into digital assets, providing them with inherent, tangible value. The infused ENJ can only be retrieved when the token is burned, a process known as "melting," ensuring that the token retains its value until it is destroyed. + +### Why ENJ Infusion is Needed + +ENJ Infusion adds a new layer of value and security to digital assets on the Enjin Blockchain. By tying tokens to a specific amount of ENJ, creators can guarantee a base value for their assets, which can: + +- Enhance user confidence and trust in the token's worth. +- Provide a tangible backing that can incentivize acquiring the token. +- Create a more stable and secure environment for trading and utilizing NFTs and s. + +### Use Cases and Scenarios + +ENJ Infusion can be utilized in various applications and games to enhance user experience and provide additional value: + +1. **Gaming Rewards:** Game developers can infuse ENJ into in-game items, ensuring players that their rewards have real-world value. For example, a rare sword in an RPG game could have 5 ENJ infused into it, making it not just valuable within the game but also outside of it. +2. **Digital Collectibles:** Creators of digital art and collectibles can infuse ENJ into their NFTs, providing buyers with a guaranteed minimum value. This can make digital art more appealing to collectors, knowing that their collectible is backed by ENJ. +3. **Loyalty Programs:** Businesses can create loyalty tokens with infused ENJ, giving customers a tangible value for their loyalty points. For instance, a company could issue tokens with 0.1 ENJ each, which customers can collect and later melt for ENJ. +4. **Crowdfunding and Fundraising:** Projects can issue tokens with infused ENJ to backers, ensuring that their contributions hold value. This can increase trust and participation in crowdfunding campaigns. +5. **In-Game Incentives:** Game developers can incentivize players to spend more time in their games by infusing additional ENJ into their items for performing certain tasks or achieving milestones. + +By integrating ENJ Infusion, creators and developers can provide enhanced value and security for their digital assets, fostering a more robust and trustworthy ecosystem on the Enjin Blockchain. + +*** + +## Creating a Token with Infused ENJ + +To create a token with Infused ENJ, proceed with the instructions on the [Creating Tokens](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) page, but make sure to specify the amount of ENJ to infuse into the token, in the “Infuse ENJ” section. +If you are using the `CreateToken` mutation, insert the infusion amount in the `params: infusion` parameter. + +:::warning Calculating the infusion amount +The Platform accepts infusion values in the **base unit** (integers), not decimal ENJ amounts. To calculate the correct input, **multiply your desired ENJ amount by 10^18** (1 quintillion). + - **Formula:** `Desired ENJ infusion` \* `1,000,000,000,000,000,000` + - **Example:** To infuse a token with **5 ENJ**, input `5000000000000000000`. +::: + +### Anyone Can Infuse + +By default, ENJ infusion is restricted only to the collection owner + +At any time, as well as at the time of creating the token, the collection owner can select to to allow anyone to add ENJ infusion to the token. +This can be done with the `CreateToken` mutation by setting the `params: anyoneCanInfuse` parameter to `true`. +If the token is already created, the `anyoneCanInfuse` state can be adjusted via the `MutateToken` mutation, with the `mutation: anyoneCanInfuse` parameter. + +:::warning **Feature Availability Notice** +Currently, the `anyoneCanInfuse` state can only be configured at the time of creating the token. The option to adjust the infusion permission for an existing token via a mutation will be added later on. We will update the documentation once this feature is available. +In the meantime, if you wish to adjust infusion permission of an existing token now, please use the Enjin Console at [console.enjin.io](https://console.enjin.io). +::: + +## Infusing ENJ to existing token + +To add ENJ infusion to a token that already exists, use the `Infuse` mutation: + +:::warning Calculating the infusion amount +The Platform accepts infusion values in the **base unit** (integers), not decimal ENJ amounts. To calculate the correct input, **multiply your desired ENJ amount by 10^18** (1 quintillion). + - **Formula:** `Desired ENJ infusion` \* `1,000,000,000,000,000,000` + - **Example:** To infuse a token with **5 ENJ**, input `5000000000000000000`. +::: + + + +```graphql +mutation Infuse{ + Infuse( + collectionId: 3298 #Specify the collection ID + tokenId: {integer: 1} #Specify the token ID + amount: 5000000000000000000 #Specify the amount of ENJ to infuse + ){ + id + method + state + } +} +``` + + +``` +curl --location 'https://platform.canary.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: enjin_api_key' \ +-d '{"query":"mutation Infuse(\r\n $collection_id: BigInt!\r\n $token_id: EncodableTokenIdInput!\r\n $amount: BigInt!\r\n) {\r\n Infuse(collectionId: $collection_id, tokenId: $token_id, amount: $amount) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":3298,"token_id":{"integer":1},"amount":5000000000000000000}}' +``` + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +// Set up the mutation +var infuse = new Infuse() + .SetCollectionId(3298) // Specify the collection ID. + .SetTokenId(new EncodableTokenIdInput().SetInteger(1)) // Specify the token ID. + .SetAmount(5000000000000000000); // Specify the amount of ENJ to infuse. + +// Define and assign the return data fragment to the mutation +var transactionFragment = new TransactionFragment() + .WithId() + .WithMethod() + .WithState(); + +infuse.Fragment(transactionFragment); + +// Create and auth a client to send the request to the platform +var client = PlatformClient.Builder() + .SetBaseAddress("https://platform.canary.enjin.io") + .Build(); +client.Auth("Your_Platform_Token_Here"); + +// Send the request and write the output to the console. +// Only the fields that were requested in the fragment will be filled in, +// other fields which weren't requested in the fragment will be set to null. +var response = await client.SendInfuse(infuse); +Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); +``` + + +```cpp +// Coming Soon! +``` + + +```javascript +fetch('https://platform.canary.enjin.io/graphql', { + method: 'POST', + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + body: JSON.stringify({ + query: ` + mutation Infuse( + $collection_id: BigInt! + $token_id: EncodableTokenIdInput! + $amount: BigInt! + ) { + Infuse(collectionId: $collection_id, tokenId: $token_id, amount: $amount) { + id + method + state + } + } + `, + variables: { + collection_id: 3298, //Specify the collection ID + token_id: {integer: 1}, //Specify the token ID + amount: 5000000000000000000 //Specify the amount of ENJ to infuse + } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.canary.enjin.io/graphql', { + query: ` + mutation Infuse( + $collection_id: BigInt! + $token_id: EncodableTokenIdInput! + $amount: BigInt! + ) { + Infuse(collectionId: $collection_id, tokenId: $token_id, amount: $amount) { + id + method + state + } + } + `, + variables: { + collection_id: 3298, //Specify the collection ID + token_id: {integer: 1}, //Specify the token ID + amount: 5000000000000000000 //Specify the amount of ENJ to infuse + } +}, { + headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation Infuse( + $collection_id: BigInt! + $token_id: EncodableTokenIdInput! + $amount: BigInt! +) { + Infuse(collectionId: $collection_id, tokenId: $token_id, amount: $amount) { + id + method + state + } +} +''' + +variables = { + 'collection_id': 3298, #Specify the collection ID + 'token_id': {'integer': 1}, #Specify the token ID + 'amount': 5000000000000000000 #Specify the amount of ENJ to infuse +} + +response = requests.post('https://platform.canary.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +) +print(response.json()) +``` + + + +Note that you are only allowed to Infuse existing token if you are the collection owner, or if the token's `anyoneCanInfuse` state is set to `True`. diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/06-the-multiverse.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/06-the-multiverse.md new file mode 100644 index 0000000..62f9571 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/06-the-multiverse.md @@ -0,0 +1,169 @@ +--- +title: "The Multiverse" +slug: "the-multiverse" +description: "Learn how to integrate the multiverse into your game or application, allowing users to access cross-game blockchain assets and experiences." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +![Using the multiverse](/img/guides/managing-users/using-the-multiverse.jpg) + + is the world's first cross-game item economy, that offers you a powerful growth engine to attract new players into your game and keep them coming back every month. + +When you join, you will be promoted on the [Multiverse Codex](https://enjin.io/multiverse), which acts as an inbound funnel that showcases your game and gives players more reasons to try it. + +These players can then participate in , a monthly event where you can reward them with when they play your game. + +In return, you’re asked to add to the Multiverse Items you distribute, this makes them more desirable to players as they endlessly grow in power. + +This creates an self-sustaining cycle that powers your game's growth: + +1. **Ecosystem-Wide Promotion Fuels Player Attraction**: Multiverse Quests are promoted across the entire ecosystem, drawing in players who are interested in earning valuable cross-game items. This shared onboarding funnel not only boosts individual games but strengthens the whole ecosystem. + +2. **Monthly Rewards Fuel Player Retention**: By offering new rewards each month, players have a continuous reason to return, keeping the community lively and excited. + +3. **Growing Playerbase Attracts More Developers:** As the player community expands, more developers are drawn to the ecosystem, eager to tap into this engaged audience. They adopt the Multiverse items, which in turn gives new and existing players even more reasons to continue playing the existing games to earn these ever-evolving items. + +4. **Utility of Multiverse Items Expands**: As more developers participate, the utility of Multiverse items increases across a wider range of games, making them even more appealing to players and attracting new players into the ecosystem. + +This cycle—driven by consistent player engagement, developer involvement, and expanding item utility—creates a powerful loop that forms a self-reinforcing growth engine. + +## Using the Primythical Chest to Acquire Players + +The is an infinite supply promotional tool and Enjin's primary funnel for onboarding new players into your game. + +Enjin is giving away this item for free, then educating players on where they can use it. + +Players can receive the item by scanning an QR code to get it for free. + +![](/img/guides/managing-users/using-the-multiverse-2.jpg) + +Once they receive the item, they are directed to the [Multiverse Codex](https://enjin.io/multiverse), where they can find games to play where their new item has utility. This is how your game will get discovered. + +Players will also start to discover a handful of other that they can earn by playing yours and other games. + +This will lead them to participate in , where they will play your game every month to earn more Multiverse Items. + +![](/img/guides/managing-users/using-the-multiverse-3.jpg) + +### How to Add Your Game + +To add your game to the [Multiverse Codex](https://enjin.io/multiverse), follow these steps: + +1. Join the [Enjin Spark Program](https://enjin.io/join). +2. Adopt at least five Multiverse Items, including the Primythical Chest. + +In return, your game will be promoted on the [Multiverse Codex](https://enjin.io/multiverse). While Enjin will help drive players to your game, it's your responsibility to convert their interest into lasting engagement. + +Your placement on each item's codex will depend on the quality of your utility and the video content you provide. + +## Using Multiverse Quests to Retain Players + +Multiverse Items are designed as powerful tokens with across multiple games. Both new and existing players are always eager to collect these items, especially as they see their utility expand with adoption by more games. + +Each month, Enjin will supply you with Multiverse Items to attract players eager to earn them in your game. + +Monthly events will feature new opportunities for players to win different Multiverse Items in your game, creating ongoing engagement between you and the Enjin playerbase. + +Over time, players may aim to collect all available items or trade them on [NFT.io](https://nft.io). + +![](/img/guides/managing-users/using-the-multiverse-4.png) + +To enter the quest, players will need to follow these steps: + +**1. Download the [Enjin Wallet](https://enjin.io/technology/wallet)** + +- Receive, secure, and manage your cross-game inventory. Available on [Android](https://play.google.com/store/apps/details?id=com.enjin.mobile.wallet&referrer=utm_source%3Dorganic_software_wallet%26utm_medium%3DOrganic%26utm_term%3Dsoftware_wallet_LP%26utm_content%3DDownload_Icon) or [iOS](https://apps.apple.com/us/app/enjin-cryptocurrency-wallet/id1349078375?ls=1). + +**2. Link Their Enjin Wallet to [Discord](https://enj.in/discord)** + +- Go to the [#link-wallet](https://enj.in/discord) channel in Enjin’s Discord server and follow the prompts. + +**3. Start Their First Multiverse Quest** + +- Find the [#multiverse-quests](https://enj.in/discord) channel in Enjin’s Discord to see a full list of games that are dropping this coveted item. + +![](/img/guides/managing-users/using-the-multiverse-5.jpg) + +### How to Add Your Game + +As new games join The Multiverse, the utility and demand for these items will continue to grow, giving players more motivation to earn them by playing your game. + +To access a supply of Multiverse Items and have Enjin promote your game as a location where players can earn these items, you must: + +1. Join the [Enjin Spark Program](https://enjin.io/join). +2. Adopt at least five Multiverse Items, including the Primythical Chest. +3. Adopt the item featured in the current quest. + +While Enjin helps drive players to your game, it’s up to you to convert their interest into sustained engagement. + +Every month, Enjin will launch a new Multiverse Quest focused on a different item in the collection. + +No new items are planned for this collection, meaning that as more developers enter the ecosystem, they will only need to add utility to the existing items to participate in Multiverse Quests. This will lead to these items growing in utility over time. + +To stimulate adoption of these items, Enjin will mint new supply access each month. Developers use this supply to attract and retain players. However, as long as the utility of the items and the player base in the ecosystem grow faster than the supply of Multiverse Items, the benefits for both developers and players are clear. + +Games with the highest monthly active users will receive a greater share of the reward supply. However, this metric for supply allocation may change in the future. + +Here is the yearly release schedule for Multiverse Items & Quests: + +| Quest | Date | Prize Pool | +| :--- | :--- | :--- | +| The Search for the Shadowsong | October 1st to October 15th | 100 x Shadowsongs | +| The Timeless Quarter of Tramyarus | November 1st to November 15th | 100 x Tramyarus Quarters | +| Mike's Misadventures | December 1st to December 15th | 500 x Mikes | +| The Nethermiser’s Vengance | January 1st to January 15th | 150 x APG-M55s | +| The Mask of the Mage-King | February 1st to February 15th | 100 x Masks of U'thuchul | +| The Blood of the Wanderer | March 1st to March 15th | 200 x Wanderer's Elixirs | +| The Pegasoid's Pursuit | April 1st to April 15th | 100 x Pegasoid Steeds | +| The Soulshift’s Embrace | May 1st to May 15th | 200 x Soulshift Armors | +| The Summoning of the Starbow | June 1st to June 15th | 100 x Starbows | +| Forging the Titan's Hammer | July 1st to July 15th | 350 x Forgehammers | +| The Epoch of the Epochrome | August 1st to August 15th | 400 x Epochrome Swords | +| The Siege of Stormwall | September 1st to September 15th | 250 x Stormwalls | + +## How to Add Multiverse Items into Your Game + +### Step 1: Download the 3D Multiverse Models + +Visit [Enjin's public resource repository](https://drive.google.com/drive/folders/1ijlDn_f9PN8GsT9kQfGZR0ObrUgEL2ge) to download 3D models for Unity and Unreal Engine. + +These ready-to-integrate models will streamline your multiverse integration process. + +You are also welcome to design your own 3D or 2D models to better suit your game's aesthetic, provided they are recognizable to players as the Multiverse Items. + +### Step 2: Add Utility to the Multiverse Tokens. + +To add utility to multiverse items, your game will need to: + +1. [Link player wallets to your game](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md). +2. [Read the inventory from player wallets](/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md). +3. Validate ownership of the [multiverse items](#the-multiverse-collection). +4. Provide an in-game benefit for each multiverse item. + +Once you have completed these steps, [contact Enjin](https://enjin.io/contact) to get listed on [The Multiverse Codex](https://enjin.io/multiverse) and collaborate on Multiverse marketing initiatives aimed at driving players into your game. + +## The Multiverse Collection + +Collection ID: `2967` + +To see if a user owns a multiverse item, use the [GetTokens query](/03-api-reference/01-queries/03-tokens-queries.md#gettokens) and filter based on Collection ID or Token IDs. + +| Token Name | Token IDs (Ranging From) | TokenIDs (Ranging To) | +| :--- | :--- | :--- | +| [Primythical Chest](https://nft.io/asset/2967-106338239662793273429419659195790131200) | 106338239662793273429419659195790131200 | N/A (/Stackable Tokens) | +| [Aeonclipse Key](https://nft.io/asset/2967-106338239662793274425543839176105918464) | 106338239662793274425543839176105918464 | N/A (/Stackable Tokens) | +| [Archspire](https://nft.io/asset/2967-106338239662793273447866403269499682816) | 106338239662793273447866403269499682816 | N/A (/Stackable Tokens) | +| [Tramyarus Quarter](https://nft.io/asset/2967-106338239662793367710728619925308440576/) | 106338239662793367710728619925308440576 | N/A (/Stackable Tokens) | +| [Epochrome Sword](https://nft.io/asset/2967-107002853660685728525072975374659354626) | 107002853660685728525072975374659354625 | 107002853660685728543519719448368906239 | +| [Forgehammer](https://nft.io/asset/2967-107002853660685728543519719448368909321) | 107002853660685728543519719448368906241 | 107002853660685728561966463522078457855 | +| [Shadowsong](https://nft.io/asset/2967-107002853660685728617306695743207113450) | 107002853660685728617306695743207112705 | 107002853660685728635753439816916664319 | +| [Oindrasdain](https://nft.io/asset/2967-107002853660685728488179487227240254752) | 107002853660685728488179487227240251393 | 107002853660685728506626231300949803007 | +| [Stormwall](https://nft.io/asset/2967-107002853660685728580413207595788010096) | 107002853660685728580413207595788009473 | 107002853660685728598859951669497561087 | +| [Soulshift Armor](https://nft.io/asset/2967-107002853660685728598859951669497561717) | 107002853660685728598859951669497561089 | 107002853660685728617306695743207112703 | +| [APG-M55](https://nft.io/asset/2967-107002853660685728561966463522078458759) | 107002853660685728561966463522078457857 | 107002853660685728580413207595788009471 | +| [Mike](https://nft.io/asset/2967-107002853660685728506626231300949809368) | 107002853660685728506626231300949803009 | 107002853660685728525072975374659354623 | +| [Starbow](https://nft.io/asset/2967-107002853660685760308813014376216789552) | 107002853660685760308813014376216788993 | 107002853660685760327259758449926340607 | +| [The Mask of U'thuchul](https://nft.io/asset/2967-107002853660685751233014930111117393999) | 107002853660685751233014930111117393921 | 107002853660685751251461674184826945535 | +| [Wanderer's Elixir](https://nft.io/asset/2967-107002853660685751251461674184826946411) | 107002853660685751251461674184826945537 | 107002853660685751269908418258536497151 | +| [Pegasoid Steed](https://nft.io/asset/2967-107002853660685760327259758449926341290) | 107002853660685760327259758449926340609 | 107002853660685760345706502523635892223 | diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/_category_.json new file mode 100644 index 0000000..5f198d3 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Advanced Mechanics" +} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/_dispatch-transactions-using-fuel-tanks.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/_dispatch-transactions-using-fuel-tanks.md new file mode 100644 index 0000000..84ef634 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/_dispatch-transactions-using-fuel-tanks.md @@ -0,0 +1,5 @@ +--- +title: "Dispatch Transactions Using Fuel Tanks" +slug: "dispatch-transactions-using-fuel-tanks" +--- +BLOCKED BY PLATFORM BUG diff --git a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/01-getting-started.md b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/01-getting-started.md new file mode 100644 index 0000000..7e86e03 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/01-getting-started.md @@ -0,0 +1,208 @@ +--- +title: "Getting Started" +slug: "getting-started" +description: "Learn how to get started with the Enjin SDK, your first step toward building powerful blockchain-based applications and games." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +To have the SDK connect with the Enjin Platform we must use its platform clients and event services. As a starter, we will look over how to set up these clients. + +:::warning Please always make sure to integrate authentication endpoints via secure backend servers. +Direct integration of the SDKs into game clients, which can be decompiled, is strictly not recommended due to potential security risks and exposure of your keys. +::: + +## Platform Clients + +For communicating with the platform's GraphQL API, the SDK defines an `IPlatformClient` interface that works with GraphQL objects for sending requests and receiving responses from the platform. + +### Creating the Client + +The built-in `PlatformClient` class, which implements `IPlatformClient`, utilizes a builder pattern for instantiating new instances. When using this builder, one of the essential inputs we must set is the base address of the platform we will be using. + + + +```csharp +using Enjin.Platform.Sdk; + +IPlatformClient client = PlatformClient + .Builder() + .SetBaseAddress("https://") + .Build(); +``` + + +```cpp +#include "EnjinPlatformSdk/PlatformClient.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +unique_ptr client = PlatformClient::Builder() + .SetBaseAddress("https://") + .Build(); +``` + + + +### Authenticating the Client + +Once we have our client instance we may now authenticate it using an authentication token for our platform. + + + +```csharp +client.Auth(""); +``` + + +```cpp +client->Auth(""); +``` + + + +### Disposing of the Client + +When we no longer need our platform client, we may dispose of it to free up any system resources it may be using with the appropriate method as shown: + + + +```csharp +client.Dispose(); +``` + + +```cpp +// We may reset the pointer or allow the class destructor to handle +// this when our client goes out-of-scope. + +client.reset(); +``` + + + +## Event Services + +The platform broadcasts events that we may respond to or gather information from. For interfacing with an event broadcasting service the SDK offers the `IEventService` interface. Whether we decide to use a free, paid, self-hosted, or any such framework as the driver for broadcasting platform events, the `IEventService` interface defines what operations we may use for working with it. + +### Creating the Service + +The default framework used by the Enjin Platform Cloud and platforms using the Enjin Platform - Starter Template for broadcasting cloud events is [Pusher Channels](https://pusher.com/channels). To work with this framework the SDK has its own `PusherEventService` which implements `IEventService` and serves as an abstraction between us and Pusher for subscribing and listening for events and is a useful tool for those just getting started. + +Our builder for the `PusherEventService` typically expects two attributes to be set before building with a third optional attribute for encryption. The two essential attributes are: + +- The application key +- The hostname of our platform + +:::info +If our application key is not valid, then we may expect a 404 error when connecting our event service. +::: + +The other optional attribute we may set is the encryption status. This will determine which protocol our service will connect with (`wss` or `ws`). If we do not set this attribute, then our builder will default to the `WebSocket Secured` protocol on building. + +An example of building this event service can be seen below: + + + +```csharp +using Enjin.Platform.Sdk; + +IEventService service = PusherEventService + .Builder() + .SetKey("websocket") + .SetHost("") + .SetEncrypted(true) // Defaults to true if not set + .Build(); +``` + + +```cpp +#include "EnjinPlatformSdk/PusherEventService.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +unique_ptr service = PusherEventService::Builder() + .SetKey("websocket") + .SetHost("") + .SetEncrypted(true) // Defaults to true if not set + .Build(); +``` + + + +:::tip +If you wish to change the key (websocket), you can do so in the platform config files. +::: + +::: note +For developers who opt to utilize Pusher's own Channels service for broadcasting events, then instead of `SetHost()` we may use the builder's `SetCluster()` method to connect our event service to the appropriate Pusher cluster. +::: + +### Managing the Connection + +Before we can use our event service, we must connect it to the designated server host through its ConnectAsync method as shown below: + + + +```csharp +using System.Threading.Tasks; + +Task task = service.ConnectAsync(); +``` + + +```cpp +#include + +using namespace std; + +future fut = service->ConnectAsync(); +``` + + + +To disconnect the service from the server, we make a call to its DisconnectAsync method. + + + +```csharp +using System.Threading.Tasks; + +Task task = service.DisconnectAsync(); +``` + + +```cpp +#include + +using namespace std; + +future fut = service->DisconnectAsync(); +``` + + + +## Disposing of the Service + +When we no longer need our event service, we may dispose of it to free up any system resources it may be using with the appropriate method as shown: + + + +```csharp +service.Dispose(); +``` + + +```cpp +// We may reset the pointer or allow the class destructor to handle +// this when our event service goes out-of-scope. + +service.reset(); +``` + + diff --git a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/02-graphql-requests.md b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/02-graphql-requests.md new file mode 100644 index 0000000..b08b74f --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/02-graphql-requests.md @@ -0,0 +1,467 @@ +--- +title: "GraphQL Requests" +slug: "graphql-requests" +description: "Learn how to use GraphQL requests to interact with the Enjin platform, allowing you to query and mutate blockchain data efficiently." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Creating the Request + +The most important step in sending a request is to first create said request. The SDK provides us with two ways to create a request. The first way is to utilize a predefined, dynamic request class to programmatically compile our request at runtime. The second way is to provide our own query string to be sent along with any relevant information, however, this method of going about creating a request places a greater demand on us to validate that our queries and mutations are indeed correct. + +### Dynamic Requests + +Dynamic requests represent the different queries and mutations we may make in the platform's GraphQL schemas and offer a built-in, programmatic method of arranging requests at runtime. + +### Setting Result Fields + +Requests which produce non-scalar types in the response will have an associated fragment class that may be passed to its aptly named `Fragment` method. For predefined type models in the SDK, there will be corresponding fragment classes that allow us to specify the fields we want to be returned in the response. + +As an example, the `GetCollection` request accepts a `CollectionFragment` instance as its and through the fragment we may specify that we want the `collectId` field to be returned in the data of the response by using a predefined method as shown below: + + + +```csharp +using Enjin.Platform.Sdk; + +CollectionFragment fragment = new CollectionFragment() + .WithCollectionId(); + +req.Fragment(fragment); +``` + + +```cpp +#include "EnjinPlatformSdk/CollectionFragment.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +CollectionFragmentPtr fragment = make_shared(); +fragment->WithCollectionId(); + +req.SetFragment(fragment); +``` + + + +We may also set the fragment to include fields not specified by the SDK if needed by using the general `WithField` method. For this method, we must pass a string as the first parameter containing the name of the field we would like to request and for the second parameter we may either pass a Boolean when requesting a scalar field or an instance of a class implementing which `IGraphQlFragment` for a non-scalar field. An example of this can be seen below: + + + +```csharp +using Enjin.Platform.Sdk; + +CollectionFragment fragment = new CollectionFragment() + .WithField("collectionId", true) + .WithField("owner", new WalletFragment().WithId()); +``` + + +```cpp +#include "EnjinPlatformSdk/CollectionFragment.hpp" +#include "EnjinPlatformSdk/WalletFragment.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +WalletFragmentPtr walletFragment = make_shared(); +walletFragment->WithId(); + +CollectionFragmentPtr fragment = make_shared(); +fragment->WithField("collectionId", true) + .WithField("owner", walletFragment); +``` + + + +:::warning +For requests which accept fragments, a fragment with at least one field requested must be passed or the SDK will throw an exception. +::: + +### Setting Request Variables + +GraphQL requests that have settable variables will have methods prefixed with "set" for setting predefined parameters for the given type. An example of this for the `GetCollection` the query can be seen below: + + + +```csharp +using Enjin.Platform.Sdk; + +GetCollection req = new GetCollection() + .SetCollectionId(2000) +``` + + +```cpp +#include "EnjinPlatformSdk/GetCollection.hpp" +#include "EnjinPlatformSdk/SerializableString.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +SerializableStringPtr value = make_shared("2000"); + +GetCollection req = GetCollection() + .SetCollectionId(value); +``` + + + +Such classes also have a general `SetVariable` method which accepts a string for the parameter name, a string for the variable's type, and a value. This can be used to set variables that are not predefined by the SDK. An example of what this may look like for the `GetCollection` can be seen below: + + + +```csharp +using Enjin.Platform.Sdk; + +GetCollection req = new GetCollection() + .SetVariable("collectionId", "BigInt!", 2000); +``` + + +```cpp +#include "EnjinPlatformSdk/GetCollection.hpp" +#include "EnjinPlatformSdk/SerializableString.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +SerializableStringPtr value = make_shared("2000"); + +GetCollection req = GetCollection() + .SetVariable("collectionId", "BigInt!", value); +``` + + + +Each component of the SDK also comes with a static class containing string fields representing the types present for that component's GraphQL schema and offer a more programmatic option for setting the type of a variable, as shown below: + + + +```csharp +#include "EnjinPlatformSdk/GetCollection.hpp" +#include "EnjinPlatformSdk/SerializableString.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +SerializableStringPtr value = make_shared("2000"); + +GetCollection req = GetCollection() + .SetVariable("collectionId", CoreTypes::BigInt, value); +``` + + +```cpp +#include "EnjinPlatformSdk/GetCollection.hpp" +#include "EnjinPlatformSdk/SerializableString.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +SerializableStringPtr value = make_shared("2000"); + +GetCollection req = GetCollection() + .SetVariable("collectionId", CoreTypes::BigInt, value); +``` + + + +### Setting Input Parameters + +Classes, such as fragments and those modeling a GraphQL input may have settable parameters to be used as part of a request. An example of one of these parameter holders is shown below: + + + +```csharp +using System.Numerics; +using Enjin.Platform.Sdk; + +BigInteger integer = BigInteger.Parse(""); + +EncodableTokenIdInput tokenId = new EncodableTokenIdInput() + .SetInteger(integer); +``` + + +```cpp +#include "EnjinPlatformSdk/EncodableTokenIdInput.hpp" +#include "EnjinPlatformSdk/SerializableString.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +SerializableStringPtr integer = + make_shared(""); + +EncodableTokenIdInputPtr tokenId = make_shared(); +tokenId->SetInteger(integer); +``` + + + +These classes also have a general `SetParameter` method for setting parameters not already defined by the SDK. These parameters may be of a type that implements `IGraphQlParameter` which itself may be a parameter holder, a list of types implementing the aforementioned interface, or a general value representing parameters that do not support nesting such as a scalar. Usage of this method can be seen below: + + + +```csharp +using System.Numerics; +using Enjin.Platform.Sdk; + +BigInteger integer = BigInteger.Parse(""); + +EncodableTokenIdInput tokenId = new EncodableTokenIdInput() + .SetParameter("integer", integer); +``` + + +```cpp +#include "EnjinPlatformSdk/EncodableTokenIdInput.hpp" +#include "EnjinPlatformSdk/SerializableString.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +SerializableStringPtr integer = + make_shared(""); + +EncodableTokenIdInputPtr tokenId = make_shared(); +tokenId->SetParameter("integer", integer); +``` + + + +:::tip +Parameters on the root fragment of a request will be treated as if they are parameters for the request itself, such is the case for the after and first parameters of the ConnectionFragment class. +::: + +### Sending the Request + +When sending requests the SDK offers built-in methods that complement the predefined requests. The methods will be prefixed with "Send" followed by the name of the request. An example of what this may look like for the `GetCollection` the request is shown below: + + + +```csharp +using Enjin.Platform.Sdk; // Namespace of extension method + +// Returns a Task containing the response +client.SendGetCollection(req); +``` + + +```cpp +#include "EnjinPlatformSdk/CoreQueries.hpp" + +using namespace enjin::platform::sdk; + +// Returns a Future containing the response +SendGetCollection(*client, req); +``` + + + +### Static Requests + +Aside from the dynamic queries and mutations built into the SDK, we may pass static query strings and variable mappings to the platform client in cases where speed is valued more than dynamic request creation. + +To facilitate static requests, the platform client implements a method that takes a string for the query body, a nullable mapping for any variables, and a relative path for the request's schema in the API. + +:::warning +The type for the data in the GraphQL query string must be aliased as result to ensure that the returned data may be properly deserialized for the response. +::: + +### Without Variables + +When creating a static request without passing a mapping of variables we need only set the values for such variables in the query string we will be using. After creating our query string we then pass it and the schema path to the `SendRequest` method while leaving the variables argument as `null`, as shown below: + + + +```csharp +using Enjin.Platform.Sdk; + +string query = @"query { + result: GetCollection(collectionId: 2000) { + collectionId + } +}"; +IPlatformRequest req = PlatformRequest + .Builder() + .SetPath("") + .AddOperation(query, null) + .Build(); + +// Returns a Task containing the response +client.SendRequest>(req); +``` + + +```cpp +#include "EnjinPlatformSdk/Collection.hpp" +#include "EnjinPlatformSdk/GraphQlResponse.hpp" +#include "EnjinPlatformSdk/PlatformRequest.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +string query = R"(query { + result: GetCollection(collectionId: 2000) { + collectionId + } +})"; + +PlatformRequestPtr req = PlatformRequest::Builder() + .SetPath("") + .AddOperation(query, {}) + .Build(); + +// Returns a Future containing the response +client->SendRequest>(req); +``` + + + +:::warning +For the C++ SDK, if the request contains an Upload variable then an additional map containing the names and values of the uploads will need to be passed to the AddOperation method. +::: + +### Handling the Response + +Once we have sent our request to the platform we should expect to get a response back with information that we intended to retrieve in the first place or with information that we may act on if we so choose. + +### Getting the Response + +The send methods of a platform client return asynchronous handlers which contain the result of our operation. At the first level of the result, is a platform response of type, which generally holds the HTTP information of the response from the platform. Within this platform response, there is also a `GraphQlResponse` property, which contains the result data of our GraphQL request and any errors that may have been returned with it. + + + +```csharp +using System.Threading.Tasks; +using Enjin.Platform.Sdk; + +GetCollection req = /* Creating the request... */ + +// Send the request and get the Task containing the response +Task>> task = + client.SendGetCollection(req); + +// Get the platform response holding the HTTP data +IPlatformResponse> platformRes = task.Result; + +// Get the result, a GraphQL response, holding the GraphQL data +GraphQlResponse gqlRes = platformRes.GqlResponse; +``` + + +```cpp +#include "EnjinPlatformSdk/Collection.hpp" +#include "EnjinPlatformSdk/CoreQueries.hpp" +#include "EnjinPlatformSdk/GraphQlResponse.hpp" +#include "EnjinPlatformSdk/IPlatformResponse.hpp" +#include +#include + +using namespace enjin::platform::sdk; +using namespace std; + +GetCollection req = /* Creating the request... */ + +// Send the request and get the Future containing the response +future>> fut = + SendGetCollection(*client, req); + +// Get the platform response holding the HTTP data +PlatformResponsePtr> platformRes = fut.get(); + +// Get the result, a GraphQL response, holding the GraphQL data +const optional>& gqlRes = platformRes->GetResult(); +``` + + + +### Processing the Result + +To process the result we must first get the `GraphQlData` property within the `GraphQlResponse`, then we will get the result property from the data. The type of result will be either one of the predefined type models or a scalar value depending on our request. + +The response also has a property indicating whether the request was successful, indicating that a result is present, and we ought to consider checking if this property is `true` before retrieving the result. + + + +```csharp +using Enjin.Platform.Sdk; + +if (gqlRes.IsSuccess) +{ + Collection collection = gqlRes.Data.Result; + + /* Handle the result */ +} +``` + + +```cpp +#include "EnjinPlatformSdk/Collection.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +if (gqlRes.has_value() && gqlRes->IsSuccess()) +{ + const optional& collection = gqlRes->GetData()->GetResult(); + + /* Handle the result */ +} +``` + + + +### Processing Errors + +The response may also contain errors that the platform may return in situations where our request was malformed, had incorrect arguments, or failed some other validation. To retrieve any such errors we make a call to the response's Errors property as shown below: + + + +```csharp +using System.Collections.Generic; +using Enjin.Platform.Sdk; + +if (gqlRes.HasErrors) +{ + IEnumerable errors = gqlRes.Errors; + + /* Handle the errors */ +} +``` + + +```cpp +#include "EnjinPlatformSdk/GraphQlError.hpp" +#include +#include + +using namespace enjin::platform::sdk; +using namespace std; + +if (gqlRes.has_value() && gqlRes->HasErrors()) +{ + const optional>& errors = gqlRes->GetErrors(); + + /* Handle the errors */ +} +``` + + + +The response also has a `HasErrors` property, which indicates whether there are in fact any errors at all and is a property we should check before making an attempt to retrieve them. diff --git a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/03-platform-events.md b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/03-platform-events.md new file mode 100644 index 0000000..9157055 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/03-platform-events.md @@ -0,0 +1,392 @@ +--- +title: "Platform Events" +slug: "platform-events" +description: "Explore how to monitor and handle platform events in the Enjin SDK, ensuring real-time updates for your blockchain applications." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Event Listeners + +To allow our application to receive events from the platform the SDK comes with an `IEventListener` interface for us to use to receive events for processing. + +### Creating a Listener + +To use the `IEventListener` interface on a listener class we create we must implement its `OnEvent` method. This method accepts a `PlatformEvent`, which contains the name of the event, the channel it was broadcasted on, the raw event message, and the parsed JSON data representing the message. A generic listener implementing this interface is shown in the code block below: + + + +```csharp +using System.Text.Json; +using Enjin.Platform.Sdk; + +class EventListener : IEventListener +{ + public void OnEvent(PlatformEvent evt) + { + string eventName = evt.EventName; + string channelName = evt.ChannelName; + string message = evt.Message; + JsonElement data = evt.Data; + + /* Handle event */ + } +} +``` + + +```cpp +#include "EnjinPlatformSdk/IEventListener.hpp" +#include "EnjinPlatformSdk/PlatformEvent.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +class EventListener : public virtual IEventListener +{ +public: + void OnEvent(const PlatformEvent& evt) override + { + const string& eventName = evt.GetEventName(); + const string& channelName = evt.GetChannelName(); + const string& message = evt.GetMessage(); + + /* Handle event */ + } +}; +``` + + + +### Registering a Listener + +To make use of the event listener we have created, we will need to register it with the event service so that the service may pass any cloud events it receives to the listener. To do so, we may make a call to the registration method, passing our listener as an argument as can be seen below: + + + +```csharp +IEventListener listener = /* Create event listener */ + +IEventListenerRegistration reg = service.RegisterListener(listener); +``` + + +```cpp +#include "EnjinPlatformSdk/IEventListener.hpp" +#include "EnjinPlatformSdk/IEventListenerRegistration.hpp" + +using namespace enjin::platform::sdk; + +EventListenerPtr listener = /* Make pointer containing event listener */ + +EventListenerRegistrationPtr reg = service->RegisterListener(listener); +``` + + + +By registering our listener this way, the service will pass along any event it receives from the cloud to our listener for handling. + +We may also use the registration the service returned to a way to monitor or validate our listener's state within the service. The registration comes with a getter for the matcher used by the service to filter events, as well as a property indicating whether the listener's registration with the service is still active or not. + +#### Filter Events with Registration + +The event service also provides means for registering listeners while filter out events for the given registration. This may help in the creation of listeners with narrower scopes, such as a dedicated wallet listener. Without using one of the follow methods of registration, a given listener will be notified of all events the service receives. + +One method for registering a listener with event filtering is to pass an event matcher. For any event to be passed along to the listener, the service expects the matcher to return `true` for it. Aside from this requirement the matcher may be configured however we want. + + + +```csharp +using System; +using Enjin.Platform.Sdk; + +IEventListener listener = /* Create event listener */ +Func matcher = evtName => +{ + return /* Return boolean */ +}; + +// Returns a registration +service.RegisterListenerWithMatcher(listener, matcher); +``` + + +```cpp +#include "EnjinPlatformSdk/IEventListener.hpp" +#include "EnjinPlatformSdk/IEventListenerRegistration.hpp" +#include + +using namespace enjin::platform::sdk; +using namespace std; + +EventListenerPtr listener = /* Make pointer containing event listener */ +EventMatcher matcher = [](const string& evtName) { + return /* Return boolean */ +}; + +// Returns a registration +service->RegisterListenerWithMatcher(listener, matcher); +``` + + + +Alternatively, we may pass our listener with an array of events to filter for or exclude using one of two methods as shown below: + + + +```csharp +using System; +using Enjin.Platform.Sdk; + +IEventListener listener = /* Create event listener */ +string[] events = { "", "", "" }; + +// Pass only the listed events to the listener +service.RegisterListenerIncludingEvents(listener, events); + +// Pass events other than the ones listed to the listener +service.RegisterListenerExcludingEvents(listener, events); +``` + + +```cpp +#include "EnjinPlatformSdk/IEventListener.hpp" +#include +#include + +using namespace enjin::platform::sdk; +using namespace std; + +EventListenerPtr listener = /* Make pointer containing event listener */ +std::set events = { "", "", "" }; + +// Pass only the listed events to the listener +service->RegisterListenerIncludingEvents(listener, events); + +// Pass events other than the ones listed to the listener +service->RegisterListenerExcludingEvents(listener, events); +``` + + + +#### Filter Events with Attribute + +Event listeners may also have an attribute attached to them that the service may refer to for filtering. The attribute takes two arguments: a Boolean stating whether the events are allowed and a list of the event names. + + + +```csharp +using Enjin.Platform.Sdk; + +[EventFilter(isAllowed: true, "", "", "")] +class EventListener : IEventListener +{ + /* ... */ +} +``` + + + ```cpp + Work in progress. + ``` + + + +Once the class has been instantiated, we may then register it using the standard `RegisterListener` method that accepts only the listener as an argument. + +:::warning +If a listener with the filter attribute is registered using any of the registration methods that accept filter arguments, then the event service may throw an exception. +::: + +#### The Events to Filter + +The SDK contains utility classes which have constants representing the names of events broadcasted by the platform. These classes allow us to programmatically set which events we want to filter or even act as a simple reference when making branch statements for the different events we receive. An example of usage for one of these utility classes, `SubstrateEvents` can be seen below: + + + +```csharp +using Enjin.Platform.Sdk; + +IEventListener listener = /* Create event listener */ +string[] events = { SubstrateEvents.CollectionCreated }; + +service.RegisterListenerIncludingEvents(listener, events); +``` + + +```cpp +#include "EnjinPlatformSdk/IEventListener.hpp" +#include "EnjinPlatformSdk/SubstrateEvents.hpp" +#include +#include + +using namespace enjin::platform::sdk; +using namespace std; + +EventListenerPtr listener = /* Make pointer containing event listener */ +std::set events = { SubstrateEvents::CollectionCreated }; + +service->RegisterListenerIncludingEvents(listener, events); +``` + + + +#### Unregistering a Listener + +To unregister a listener from the event service, we need only pass the listener instance to the service's unregister method as shown below: + + + +```csharp +// Passing an event listener instance +service.UnregisterListener(listener); +``` + + +```cpp +// Passing an event listener reference +service->UnregisterListener(*listener); // Reference from pointer +``` + + + +We may also unregister all listeners from the service by calling the unregister all method as so: + + + +```csharp +service.UnregisterAllListeners(); +``` + + +```cpp +service->UnregisterAllListeners(); +``` + + + +After a listener is unregistered, the service will also update the `IEventRegistration` we received while registering our listener, so that the registration now returns `false` for its `IsRegistered` getter. + +### Event Subscription + +After setting up our event listener we now need to manage which event channels we want our service to listen to for it to pull events from. This enables us to have our service only listen in on channels necessary for our application's current needs, such as the wallet channel for a connected user, updates to a particular collection, or the progression of a transaction. + +#### Subscribing + +To subscribe to an event channel, we need to pass the channel name to the subscribe method, as shown below: + + + +```csharp +using System.Threading.Tasks; + +string channelName = ""; +Task task = service.SubscribeAsync(channelName); +``` + + +```cpp +#include +#include + +using namespace std; + +string channelName = ""; +future fut = service->SubscribeAsync(channelName); +``` + + + +Once subscribed, the event service will pass along any event broadcasted on the given channel to our registered listeners, so long as said listeners do not exclude the events. + +#### Unsubscribing + +To unsubscribe from an event channel, we may pass the same channel name we used to subscribe, as shown below: + + + +```csharp +using System.Threading.Tasks; + +string channelName = ""; +Task task = service.UnsubscribeAsync(channelName);; +``` + + +```cpp +#include +#include + +using namespace std; + +string channelName = ""; +future fut = service->UnsubscribeAsync(channelName); +``` + + + +We may also unsubscribe from all channels by calling the relevant method. + + + +```csharp +using System.Threading.Tasks; + +Task task = service.UnsubscribeAllAsync(); +``` + + +```cpp +#include + +using namespace std; + +future fut = service->UnsubscribeAllAsync(); +``` + + + +#### Monitoring the Service + +The event service also provides means to monitor its state through events. The following code shows the different events that may be used to receive notifications regarding state changes within the service. + + + +```csharp +// Notifies that the service has connected to a server +service.OnConnected += (sender, _) => { /* ... */ }; + +// Notifies that the service has disconnected froma server +service.OnDisconnected += (sender, _) => { /* ... */ }; + +// Notifies that the service's connection state has updated +service.OnConnectionStateChanged += (sender, state) => { /* ... */ }; + +// Notifies that the service has subscribed to an event channel +service.OnSubscribed += (sender, channelName) => { /* ... */ }; +``` + + +```cpp +PusherEventService::Builder() + // Notifies that the service has connected to a server + .SetOnConnectedHandler([]() { + /* ... */ + }) + // Notifies that the service has disconnected froma server + .SetOnDisconnectedHandler([]() { + /* ... */ + }) + // Notifies that the service's connection state has updated + .SetOnConnectionStateChangedHandler([](ConnectionState state) { + /* ... */ + }) + // Notifies that the service has subscribed to an event channel + .SetOnSubscribedHandler([](const string& channelName) { + /* ... */ + }); +``` + + diff --git a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md new file mode 100644 index 0000000..f80faf4 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md @@ -0,0 +1,15 @@ +--- +title: "Software Development Kits (SDKs)" +slug: "../software-development-kit" +description: "Get started with the Enjin SDK, offering comprehensive developer tools to integrate blockchain technology into your apps and games with ease." +--- +The Enjin Platform SDKs provide an API for communicating with the Enjin Platform. + +The Enjin Platform SDKs provide a framework that developers can use on their desired programming language as it allows games and apps to interact with the Enjin API without the need of building a bridge between your application and our API requests, the Enjin SDKs facilitate the usage of listening and processing of events emitted by the Enjin Platform, which helps us deliver a better user experience in our application and eliminate unnecessary API requests we would otherwise make to the Enjin Platform. + +## SDK Libraries + +| Library | Link | +| :------ | :------------------------------------------------------------------------- | +| C# | [/enjin/platform-csharp-sdk](https://github.com/enjin/platform-csharp-sdk) | +| C++ | [/enjin/platform-cpp-sdk](https://github.com/enjin/platform-cpp-sdk) | diff --git a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/_category_.json new file mode 100644 index 0000000..37ba922 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Software Development Kits (SDKs)" +} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/_logger.md b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/_logger.md new file mode 100644 index 0000000..d756953 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/_logger.md @@ -0,0 +1,170 @@ +--- +title: "Logger" +slug: "logger" +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Logger Interface + +The SDK provides a simple logger interface to facilitate logging within its classes and whichever logging framework we choose to use in our application. An example of this interface's implementation can be seen in the block below: + + + +```csharp +using Enjin.Platform.Sdk; + +class Logger : ILogger +{ + public void Log(LogLevel logLevel, string? message) + { + /* Log the message */ + } + + public void Log(LogLevel logLevel, Exception? e, string? message) + { + /* Log the exception and message */ + } +} +``` + + +```cpp +#include "EnjinPlatformSdk/ILogger.hpp" +#include "EnjinPlatformSdk/LogLevel.hpp" +#include +#include + +using namespace enjin::platform::sdk; +using namespace std; + +class Logger : public virtual ILogger +{ +public: + void Log(LogLevel logLevel, const string& message) override + { + } + + void Log(LogLevel logLevel, const exception& e, const string& message) override + { + } +}; +``` + + + +## Log Levels + +The `ILogger` interface uses log levels to communicate the type of message being logged. These levels are represent within the `LogLevel` enums and are summarized in the table below: + +| LogLevel | Description | +| :---------- | :-------------------------------------------------------------------------------------------- | +| Trace | For detailed messages. May expose sensitive information and should not be used in production. | +| Debug | For debugging or messages appropriate in a development environment. | +| Information | General informational messages. | +| Warning | For unexpected behavior that does not necessarily cause a failure. | +| Error | For unhandled errors or exceptions. | +| Critical | For issues that require immediate attention. | +| None | Specifies no messages are to be logged. | + +## Platform Client Logging + +To setup logging for the `PlatformClient` we may pass an `ILogger` instance to its builder as shown below: + + + +```csharp +using Enjin.Platform.Sdk; + +ILogger logger = /* Instantiate class implementing ILogger */ + +// Using a PlatformClient builder +builder.SetLogger(logger); +``` + + +```cpp +#include "EnjinPlatformSdk/ILogger.hpp" + +using namespace enjin::platform::sdk; + +LoggerPtr logger = /* Make pointer containing ILogger implementation */ + +// Using a PlatformClient builder +builder.SetLogger(logger); +``` + + + +## Logging HTTP Traffic + +The SDK also offers HTTP logging options for its built-in platform client. There are four log levels to choose from for HTTP traffic. + +| HttpLogLevel | Description | +| :----------- | :--------------------------------------------------------------------- | +| None | Indicates HTTP traffic is not to be logged. | +| Basic | Logs topical information, such as the method, URI, and content-length. | +| Headers | Logs HTTP headers in addition to topical information. | +| Body | Logs the full request/response. | + +To enable HTTP traffic logging, we must provide an `ILogger` instance and our desired HTTP log level when building the client as shown below: + + + +```csharp +using Enjin.Platform.Sdk; + +ILogger logger = /* Instantiate class implementing ILogger */ +HttpLogLevel level = /* Choose a HTTP log level */ + +// Using a PlatformClient builder +builder.SetLogger(logger) + .SetHttpLogLevel(level); +``` + + +```cpp +#include "EnjinPlatformSdk/HttpLogLevel.hpp" +#include "EnjinPlatformSdk/ILogger.hpp" + +using namespace enjin::platform::sdk; + +LoggerPtr logger = /* Make pointer containing ILogger implementation */ +HttpLogLevel level = /* Choose a HTTP log level */ + +// Using a PlatformClient builder +builder.SetLogger(logger) + .SetHttpLogLevel(level); +``` + + + +### Event Service Logging + +To setup logging for the `PusherEventService` we may pass an `ILogger` instance to its builder as shown below: + + + +```csharp +using Enjin.Platform.Sdk; + +ILogger logger = /* Instantiate class implementing ILogger */ + +// Using a PusherEventService builder +builder.SetLogger(logger); +``` + + +```cpp +#include "EnjinPlatformSdk/ILogger.hpp" + +using namespace enjin::platform::sdk; + +LoggerPtr logger = /* Make pointer containing ILogger implementation */ + +// Using a PusherEventService builder +builder.SetLogger(logger); +``` + + diff --git a/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md b/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md new file mode 100644 index 0000000..e6deb90 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md @@ -0,0 +1,179 @@ +--- +title: "Enjin Farmer: Setup Guide" +sidebar_label: "Setup Guide" +slug: "setup-guide" +description: "Follow our step-by-step guide to set up a sample Unity game with full Enjin NFT integration. Learn how to configure a client, server, and wallet to mint, transfer, and manage blockchain assets in a hands-on project." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +Welcome! This guide will walk you through setting up and running the Enjin Farm Game, a sample project demonstrating how to integrate Enjin's NFT technology into a Unity game. + +In this simple farming game, you'll plant seeds, harvest crops, and collect resources. Some resources are special and will be minted as NFTs directly to your in-game wallet. You can then view these NFTs in your inventory, them, or transfer them to an external wallet. + +### System Architecture + +The project consists of four main components that work together: + + - **[Game Client (Unity)](https://github.com/enjin/platform-sample-game-client-unity):** The game itself, where you play and interact with items. + - **[Game Server (Node.js)](https://github.com/enjin/platform-sample-game-server):** A backend API that the game client communicates with to handle all NFT-related actions like minting and transferring. + - **:** The cloud-based service that provides the core NFT infrastructure. + - **Wallet Daemon:** A secure application that manages a wallet on behalf of the game to automatically sign and approve transactions. + +----- + +## Prerequisites + +Before you begin, make sure you have the following installed: + + - ✅ **Unity Hub** with **Unity Editor version `6000.0.24f1`**. + - ✅ **Node.js** (which includes `npm`). + - ✅ **Git** for cloning the repositories. + - ✅ An **Enjin Platform account**. If you don't have one, you can create it [here](https://platform.canary.enjin.io/). + - ✅ Some cENJ tokens (can be aquired from the [cENJ Faucet](https://faucet.canary.enjin.io/)) + +----- + +## Step 1: Download Project Files + +First, you need to download the game client, the game server, and the Wallet Daemon. + +1. **Clone the Game Client:** Open a terminal or command prompt and run: + + ```bash + git clone https://github.com/enjin/platform-sample-game-client-unity.git + ``` + +2. **Clone the Game Server:** In the same terminal, run: + + ```bash + git clone https://github.com/enjin/platform-sample-game-server.git + ``` + +3. **Download the Wallet Daemon:** Download the latest executable for your operating system from the [Wallet Daemon releases page](https://github.com/enjin/wallet-daemon-ui/releases). + +----- + +## Step 2: Configure Enjin Services + +Next, you'll set up your Enjin Platform account and the Wallet Daemon. + +### Enjin Platform + +1. Log in to your [Enjin Platform](https://platform.canary.enjin.io/) account. +2. Head over to your [account settings page](https://platform.canary.enjin.io/settings). +3. Navigate to the **Daemon Wallet** section and create a new API Token. +4. Copy the **API Token**; you will need this in the next step. + +### Wallet Daemon + +1. Run the Wallet Daemon executable you downloaded. +2. Follow the on-screen instructions to configure it, and enter the **API Token** you just copied from the Enjin Platform into the Settings → Canary Matrixchain input field. For a detailed guide, see the [Wallet Daemon documentation](https://docs.enjin.io/getting-started/using-wallet-daemon#wallet-daemon-executable). +3. Select the Enjin Platform Canary in the network dropdown menu, and run the Wallet Daemon. +4. Once configured and running, the Wallet Daemon UI will display a wallet address. **Copy this wallet address** for the next step. + +----- + +## Step 3: Configure and Run the Game Server + +Now, let's set up the backend server that powers the game's NFT features. + +1. Navigate into the game server directory you cloned: `cd platform-sample-game-server`. +2. Duplicate the `.env.example` file and rename the copy to `.env`. +3. Open the `.env` file and fill in the following variables: + - `PORT=3000` (You can change this if port 3000 is already in use). + - `JWT_SECRET`: Generate another secure, random string. This is used for authenticating players. + - `ENJIN_API_URL`: Keep the default `https://platform.canary.enjin.io/graphql` for testing on the Canary network. + - `ENJIN_API_KEY`: Paste the **API Key Token** from your Enjin Platform account. + - `DAEMON_WALLET_ADDRESS`: Paste the wallet address you copied from the Wallet Daemon UI. + - `ENJIN_COLLECTION_ID`: Leave this blank for now. +4. Install the server dependencies by running `npm install`. +5. Launch the server for the first time by running `npm run dev`. + +The server will now connect to the Enjin Platform, create a new NFT collection for your game, and create the NFT tokens for the in-game resources. + +:::info **Important** +Watch the terminal logs. Once the setup is complete, the server will log the new **Collection ID**. It will look something like this: `Collection and resource tokens are ready. Using collection ID: XXXXXX`. **Copy this Collection ID** and save it. You'll need it to configure the game client. +::: + +Keep the server and the Wallet Daemon running in the background. + +----- + +## Step 4: Configure the Unity Game Client + +It's time to set up the Unity project and connect it to your game server. + +1. Open **Unity Hub**. +2. Click `Add` → `Add project from disk` and select the `platform-sample-game-client` folder you cloned earlier. +3. Open the project. +4. Once the project is open in the Unity Editor, you need to configure two things. + +#### 1. Configure the NFT Items + + - In the `Project` window, navigate to `Assets/Enjin Integration/Scripts/Data/Items`. + - You will see three `Enjin Item` assets: `GemGreen`, `GoldCoin`, and `GoldCoinBlue`. +

+ +

+ - Click on **each one** of these items. + - In the `Inspector` window for each item, find the **Collection Id** field and paste the `Collection ID` you saved from the game server's terminal log. +

+ +

+ +#### 2. Configure the connection to the Game Server + +:::note +If you are running the game server and client on the same machine, and you haven't changed the default port of 3000, you can skip this step. +::: + + - In the `Project` window, navigate to `Assets/Enjin Integration/Prefabs/`. + - Select the **EnjinManager** prefab. + - In the `Inspector` window, find the `Enjin API Service (Script)` component and configure the host property to your game server URL. If you are running the game server and client on the same machine, the default `http://localhost:3000` is correct. + +

+ +

+ +----- + +## Step 5: Play the Game! 🎮 + +You're all set up and ready to play. + +1. In the Unity Editor's `Project` window, navigate to `Assets/HappyHarvest/Scenes`. +2. Double click on the `Farm_Outdoor` scene. +3. Press the **Play** button at the top of the Unity Editor to launch the game. +4. Look at the **Console** window in the editor. You should see a log message: `Server connection successful (Health Check: OK)`. +

+ +

+ :::warning + If you see an error, double-check that your server is running and that the `Host` and `App Key` in the `EnjinManager` are correct. + ::: +5. In the game, click the **Menu** button (top-right), then **Login**. +

+ +

+6. Enter an email and password and click **Login**. This will register a new user and create a managed wallet for your player on the Enjin Platform. +7. Close the menu and use the **W, A, S, D** keys to move your character. +8. Walk up to a crop and click on it to harvest it. Keep harvesting until a resource item pops out. +

+ +

+9. Click on the resource item to collect it. This action tells the game server to mint that item as an NFT to your player's wallet. +10. From the inventory, you can click **Melt** to destroy the NFT or enter another wallet address in the **Transfer Recipient** field and click **Send** to send it to someone else. +

+ +

+:::warning cENJ funds are required +New managed wallet have no funds. To melt or transfer tokens out of a managed wallet, you'll need to fund it with some cENJ, or set up a fuel tank. +To receive cENJ funds for testing, use the [cENJ faucet](https://faucet.canary.enjin.io/) +::: + +:::info Understanding the code +To learn more about the implementation and dive deep into the game client and server code, continue to the [Enjin Farmer: Implementation Breakdown page](/02-guides/01-platform/05-enjin-farmer-sample-game/03-implementation-breakdown.md). +::: + +Happy farming! \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/03-implementation-breakdown.md b/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/03-implementation-breakdown.md new file mode 100644 index 0000000..b59ebc0 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/03-implementation-breakdown.md @@ -0,0 +1,185 @@ +--- +title: "Enjin Farmer: Implementation Breakdown" +sidebar_label: "Implementation Breakdown" +slug: "implementation-breakdown" +description: "Dive deep into the code and architecture of the Enjin Farmer sample game. This technical breakdown explains the implementation flow, from the Unity client to the Node.js game server. Understand the key GraphQL mutations and API calls used to mint, transfer, and manage NFTs in your game." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +The **Enjin Farmer** sample project demonstrates a basic Enjin Platform integration within a Unity game. It's built with a client-server architecture to ensure security and scalability. + +The project consists of two main components: + + * **🎮 Unity Game (Client):** The front-end game that players interact with. It handles gameplay, visuals, and user input, communicating with the game server to perform blockchain actions. + * **🖥️ Game Server (Backend):** A Node.js application that manages all Enjin Platform logic. It securely handles wallet creation, token minting, and other on-chain operations on behalf of the players. + +### 💡 Important Considerations + +Before you begin, please keep the following in mind: + + * **Demonstration Purpose:** This is a simplified example designed to showcase a basic integration. It is **not suitable for a production environment** as is. + * **WebSockets:** This implementation does not use [WebSocket events](/03-api-reference/03-websocket-events.md). In a real-world application, WebSockets can simplify the process of listening for transaction finalization and receiving real-time updates, such as when a user receives an NFT from an external source like the marketplace. + * **Wallet Funding:** New managed wallets are created without any funds. To cover network fees for actions like melting or transferring tokens, you must either fund each wallet individually or use a [Fuel Tank](/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md) to subsidize transactions for all your users. + +----- + +## 🖥️ Game Server + +The game server is a RESTful API built with Node.js and Express. It serves as the secure bridge between the game client and the Enjin Platform. The main entry point is the [`src/index.js` file](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/index.js). + +### Environment Variables + +The server is configured using the following environment variables: + +| Variable | Description | +| :--- | :--- | +| `PORT` | The port the server listens on. Defaults to `3000`. | +| `JWT_SECRET` | A secure, random string used for signing player authentication tokens. | +| `ENJIN_API_URL` | The Enjin Platform API URL. Use `https://platform.canary.enjin.io/graphql` for testing (Canary Network) or `https://platform.enjin.io/graphql` for production. | +| `ENJIN_API_KEY` | Your API Key Token obtained from the Enjin Platform. | +| `DAEMON_WALLET_ADDRESS` | The address of your Wallet Daemon. This wallet receives the initial supply of all created tokens. | +| `ENJIN_COLLECTION_ID` | The ID of the Enjin Farmer collection. If left blank, the server will create a new collection on startup. | + +### Server Initialization & Collection Setup + +On startup, the server performs a one-time setup to ensure the necessary blockchain assets exist. + +1. **Check for Collection:** The server first checks if an `ENJIN_COLLECTION_ID` has been provided. +2. **Create Collection:** If the ID is missing, the server calls the [`createCollection` function](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L69), which executes the `CreateCollection` mutation on the Enjin Platform. The automatically signs the request. + ```graphql + mutation CreateCollection($name: String!, ...) { + CreateCollection(...) { + id + method + state + } + } + ``` +3. **Monitor Transaction:** The mutation returns a request ID. The server then [polls the `GetTransaction` query](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L15) until the transaction `state` is `FINALIZED` and the `result` is `EXTRINSIC_SUCCESS`. + ```graphql + query GetTransaction($requestId: Int!) { + GetTransaction(id: $requestId) { + state + result + events { ... } + } + } + ``` +4. **Extract Collection ID:** Once finalized, the server [extracts the new collection ID from the transaction's events](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L43) and assigns it to the `ENJIN_COLLECTION_ID` variable. +5. **Create NFTs:** Using a similar process, the server then [creates the three resource NFTs](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L116) ([Gold Coin](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L6), [Gold Coin (Blue)](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L7), and [Green Gem](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L8)) within the collection by calling the `CreateToken` mutation for each and waiting for finalization. + ```graphql + mutation CreateToken($collectionId: BigInt!, $name: String!, ...) { + CreateToken(collectionId: $collectionId, params: { ... }) { + id + method + state + } + } + ``` + +After this setup is complete, the server starts listening for API requests. + +### API Endpoints + +The server exposes several endpoints to handle game actions. The `wallet` and `token` endpoints are protected by a [JWT authentication middleware](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/middlewares/jwtAuth.js), which verifies the player's identity before processing the request. + +#### Authentication + + * [`GET /api/auth/health-check`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/auth.js#L7): A simple endpoint to verify that the server is online. + * [`POST /api/auth/register`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/auth.js#L14): Creates a new player and an associated [managed wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md). To create the wallet, it calls the [`CreateWallet` mutation](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L271), using the player's [email address as the unique `externalId`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/auth.js#L18). + ```graphql + mutation CreateWallet($externalId: String!) { + CreateWallet(externalId: $externalId) + } + ``` + * [`POST /api/auth/login`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/auth.js#L36): Logs in an existing player, returning their wallet address and a JWT. + +#### Wallet Management + + * [`POST /api/wallet/create`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/wallet.js#L28): Creates a new managed wallet + * [`POST /api/wallet/get`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/wallet.js#L8): Retrieves details for the authenticated player's managed wallet. + * [`GET /api/wallet/get-tokens`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/wallet.js#L48): Retrieves the player's managed wallet and all tokens it holds. It calls the [`GetWallet` query](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L208) (using [GraphQL Pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination) to loop through all pages of results, ensuring the complete inventory is fetched). + ```graphql + query GetWalletTokens($externalId: String!) { + GetWallet(externalId: $externalId) { + account { ... } + tokenAccounts(...) { ... } + } + } + ``` + +#### Token Actions + + * [`POST /api/token/mint`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/token.js#L8): Mints a token to the player's wallet. The server calls the [`MintToken` mutation](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L317). The recipient address is extracted from the player's authenticated session. + ```graphql + mutation mintToken($recipient: String!, $collectionId: BigInt!, ...) { + MintToken(recipient: $recipient, collectionId: $collectionId, ...) { + id + } + } + ``` + * [`POST /api/token/melt`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/token.js#L29): Melts a token from the player's wallet. This uses the [`Burn` mutation](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L371). Since the token needs to be melted from the managed wallet's account, it needs to be signed by the managed wallet. For that, the player's managed wallet address is specified as the `signingAccount`. ([Learn more about managed wallets here](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md)) + ```graphql + mutation burnToken($signingAccount: String!, $collectionId: BigInt!, ...) { + Burn(signingAccount: $signingAccount, collectionId: $collectionId, ...) { + id + } + } + ``` + * [`POST /api/token/transfer`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/token.js#L50): Transfers a token from the player's managed wallet to another address. This uses the [`SimpleTransferToken` mutation](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L425), again specifying the player's managed wallet as the `signingAccount`. + ```graphql + mutation transferToken($signingAccount: String!, $recipient: String!, ...) { + SimpleTransferToken(signingAccount: $signingAccount, recipient: $recipient, ...) { + id + } + } + ``` + +----- + +## 🎮 Unity Game + +The Unity game is the client-facing part of the project. It focuses on gameplay and user experience while offloading all sensitive blockchain operations to the game server. + +### Core Components + +The Enjin integration is managed by a few key scripts and a central prefab: + + * **`EnjinManager.prefab`**: The heart of the integration. This prefab is added to the `Farm_Outdoor` scene and configures the **Host URL** (e.g., `http://localhost:3000`) in the Inspector to connect to your game server. + * **[`EnjinManager.cs`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs)**: A singleton controller that manages the player's session (auth token, wallet data) and exposes high-level methods like `MintToken()` for other game scripts to use. + * **[`EnjinApiService.cs`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/API/EnjinApiService.cs)**: Handles all REST API communication with the game server using Unity's `UnityWebRequest`. + * **[`EnjinItem.cs`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Data/EnjinItem.cs)**: A `ScriptableObject` that represents the data of a blockchain item, such as its display name and its corresponding on-chain token ID. + * **UI Scripts** ([`BackpackUI.cs`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/UI/BackpackUI.cs), [`BackpackItemController.cs`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/UI/BackpackItemController.cs)): Scripts that manage the UI for viewing and interacting with the player's NFT inventory. + +### Initial Setup & Player Authentication + +1. **Health Check**: On launch, the client [calls the `/api/auth/health-check` endpoint](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/API/EnjinApiService.cs#L36) to ensure the server is available. +2. **Login/Register**: From the login screen, the player [clicks "Login"](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/HappyHarvest/Common/UI/SettingMenu/Script/SettingMenu.cs#L145), which calls the [`EnjinManager.Instance.RegisterAndLogin()` method](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs#L141). +3. **API Request**: This triggers `EnjinApiService` to [send a POST request to the `/api/auth/register` endpoint](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/API/EnjinApiService.cs#L69). +4. **Store Auth Token**: The server responds with a JWT authentication token. The client [saves this token locally using `PlayerPrefs`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs#L152) and [loads it on subsequent launches](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs#L40) for a seamless experience. + +### In-Game NFT Interactions + +All blockchain actions are initiated by the client but securely executed by the server. + +#### Harvesting and Minting Tokens + +When a player [harvests a crop with the Hoe tool](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/HappyHarvest/Scripts/Items/Hoe.cs#L18), they have a chance to find a resource token. + +1. An `EnjinToken` GameObject [appears on the harvested tile](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs#L218). +2. When the player collects this GameObject, its [`InteractedWith()` method](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Gameplay/EnjinToken.cs#L20) is triggered. +3. This calls [`EnjinItem.Collect()`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Data/EnjinItem.cs#L37), which in turn calls `EnjinManager.Instance.MintToken()`. +4. `EnjinManager` then uses `EnjinApiService` to [send a request to the `/api/token/mint` endpoint](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/API/EnjinApiService.cs#L97). + +#### Viewing the Wallet (Backpack UI) + +1. Clicking the backpack icon opens the inventory screen, managed by `BackpackUI.cs`. +2. The UI [calls `EnjinManager.Instance.GetManagedWalletTokens()`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/UI/BackpackUI.cs#L47), which [sends a request to the `/api/wallet/get-tokens` endpoint](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/API/EnjinApiService.cs#L205). +3. The server saves the list of tokens, and the `BackpackUI` [populates the view with the data](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/UI/BackpackUI.cs#L81). +4. The `BackpackUI` also [subscribes to the `EnjinManager.Instance.OnWalletUpdated` event](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/UI/BackpackUI.cs#L39) to automatically refresh the inventory after a token is [minted](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs#L95), [melted](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs#L107), or [transferred](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs#L119). + +#### Melting and Transferring Tokens + + * **Melting**: The player [clicks "Melt" in the backpack](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/UI/BackpackItemController.cs#L38). This flows through [`EnjinManager`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs#L100) and sends a request to the [`/api/token/melt` endpoint](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/API/EnjinApiService.cs#L126). + * **Transferring**: The player [clicks "Send"](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/UI/BackpackItemController.cs#L59), sending a request to the [`/api/token/transfer` endpoint](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/API/EnjinApiService.cs#L155). \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/_category_.json new file mode 100644 index 0000000..d3d632f --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Enjin Farmer Sample Game" +} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/06-enjin-discord-bot.md b/versioned_docs/version-2.x/02-guides/01-platform/06-enjin-discord-bot.md new file mode 100644 index 0000000..331e922 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/06-enjin-discord-bot.md @@ -0,0 +1,62 @@ +--- +title: "Enjin Discord Bot" +slug: "enjin-discord-bot" +description: "Discover how to use the Enjin Discord Bot to integrate blockchain functionality within Discord servers, enhancing community interaction." +--- +
+ +
+--- +The Enjin Discord Bot offers an advanced integration solution, enabling users to seamlessly connect their wallets. Utilizing this secure wallet connection, the bot is capable of automatically assigning specific roles based on the user's collection and NFT holdings. + +## Step #1: Add the Enjin Bot to your discord server + +Head over to this page to add the Enjin Bot to your discord server: https://enj.in/discord-bot + +## Step #2: Grant the Enjin Bot permissions + +Navigate to "**Server Settings**" within your Discord server, and select "**Roles**". +For the Enjin Bot to be able to assign roles to users, it must be positioned higher in the hierarchy than all the roles it is intended to assign. +In the roles list, locate the Enjin Bot and drag it **above** the roles it will be managing. +Make sure to save the changes by clicking the "**Save Changes**" button. + +![Setting up the Enjin Discord Bot - Configuring Roles Permissions](/img/guides/integrations/discord-roles-permissions.gif) + + +## Step #3: Setup the role assignment configurations + +To configure the role assignment, you will need to enter the specific collection IDs that the Enjin Bot will use to assign roles. + +In your Discord server, type `/setup ` or type `/` and select the `/setup` command. You will be prompted to enter the collection ID, and two optional fields `asset` ID and `balance` (which can be viewed by pressing the 'Tab' button) to assign role based on token holdings. +These IDs correspond to the collection / asset that users must own in order to be assigned certain roles. + +You can find the collection IDs by visiting [NFT.io](https://nft.io) and selecting the specific collection. The collection ID is located next to the collection name or in the token's details, where you can also find the token ID. + +After verifying and finalizing the configuration settings, send the message. + +The bot will then prompt you to select which roles to assign - Select the roles and click anywhere outside of the selection box to confirm the selection. + +![Setting up the Enjin Discord Bot - Running the Setup Command](/img/guides/integrations/discord-running-setup-command.gif) + +:::tip Did you know? +You can assign different roles based on token amount holdings. +e.g. `Enjineer` role if owning at least 1 Blobby token, and `Gamer` role if owning at least 10 Blobby tokens. +::: + +## Step #4: Configure the Wallet Connection Channel + +To enable the bot to scan user wallets, users must first establish a connection between their wallets and your Discord server. This requires a dedicated text channel for wallet connections. + +Create a specific text channel that users will utilize for this purpose. + +Within this newly created channel, execute the `/add-button` command. This command will generate a user-friendly button for users to connect their wallets. + +For a more personalized user experience, you may also add a custom description to the wallet connection button. Simply append the `description` parameter to the `/add-button` command prompt. + +![Setting up the Enjin Discord Bot - Configuring Wallet Connection Channel](/img/guides/integrations/discord-setting-up-wallet-connection.gif) + +*** + +Congratulations! You have successfully configured the Enjin Discord Bot for your server. This advanced integration will allow for seamless wallet connection and role assignment based on user's collection and NFT holdings. + +For those interested in further customization or hosting options, the Enjin Discord Bot code is open-source and publicly available at https://github.com/enjin/discord-bot. Feel free to explore, alter, and host it according to your specific needs. diff --git a/versioned_docs/version-2.x/02-guides/01-platform/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/_category_.json new file mode 100644 index 0000000..a50d670 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/01-platform/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Platform", + "link": { + "type": "generated-index", + "description": "Dive into guides for the Enjin Platform. Learn to manage tokens, users, and leverage advanced features. Use the SDKs for seamless blockchain integration into your project." + } +} + \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/02-staking-integrtion-guide.md b/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/02-staking-integrtion-guide.md new file mode 100644 index 0000000..c0ed41c --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/02-staking-integrtion-guide.md @@ -0,0 +1,669 @@ +--- +title: "Staking Integration Guide" +slug: "staking-integration" +description: "This page will cover the basic integration of Enjin Blockchain Staking functionality, enabling you and your users to get started with staking on the Enjin Blockchain." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +## Background + +[Enjin Blockchain](https://enjin.io/enjin-blockchain) is the world's first purpose-built blockchain for games and apps. Enjin is a Proof-of-Stake dual-layer chain with NFT-specific functions at the protocol level. + +Enjin Blockchain is built on-top of [Substrate](https://substrate.io/) and inherits similar staking functionalities to other Relaychains running on Substrate, with a few key differences which will be covered throughout this Integration Guide. + +## Definitions + +There are a series of definitions that’ll be used throughout this document. + +“**[Validator](/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md)**” refers to a blockchain node that is setup to produce and validate block production. + +“**Era**” refers to a Validator session. During this session, a set of Validators are elected and work together for the integrity of the blockchain. An Era, on the Enjin Relaychain, is 14,400 blocks — it is commonly considered to be “_an era is one (1) full day._” + +“**Active Validator**” refers to a Validator which has been selected to produce work in a given Era. + +“**ENJ**” refers to the native token of the Enjin Blockchain: [Enjin Coin](https://enjin.io/enjin-coin). + +**** refers to a with multiple quantities, created using the [multiToken pallet](/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md). + +“**NFT**” refers to a Non-Fungible Token created using the [multiToken pallet](/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md). + +“**Staking**” refers to the process of bonding (locking) ENJ on the Enjin Relaychain for a minimum of twenty-eight (28) eras. Staking is used to select Active Validators who will be responsible for maintaining the integrity of the blockchain. + +“**sENJ**” refers to Staked Enjin Coin, an . This represents ENJ that is locked into a Nomination Pool. The collection identifier for this token is **1**. + +“**Liquid Staking**” refers to a concept which allows users to use their sENJ for purposes such as governance or [exchanging](/04-enjin-blockchain/02-enjin-relaychain/02-stake-exchange-pallet.md). + +“**Nomination Pool”** refers to a mechanism that allows a group of users to stake their ENJ and nominate a set of Validators that represents the group. Specifically, the nominationPools pallet. + +“**Degen**” refers to an NFT that is used for the purpose of creating and managing a Nomination Pool. The collection identifier for this token is **2**. + +“**Inactive Degen**” refers to a Degen that has not yet been assigned to a Nomination Pool. + +“**Pool Member**” refers to an account that has contributed (staked) some amount of ENJ into a given Nomination Pool. + +“**Pool Owner**” refers to the account that currently holds the Degen associated with a Nomination Pool. + +“**Staker**” refers to an account that has contributed (staked) some amount of ENJ into any Nomination Pool. + +“**Stake Exchange**” refers to a pallet that is responsible for users being able to exchange their sENJ for ENJ based on offers defined by the users. + +“**Stake Factor”** refers to a Nomination Pool’s total bonded ENJ divided by the total supply of sENJ. This value is multiplied against your sENJ contribution, for that Nomination Pool, to derive the total amount of ENJ unbondable. + +“**Pool Deposit**” refers to the minimum of **2,500 ENJ** required to create a Nomination Pool. + +“**Pool State**” refers to the status associated with a Nomination Pool. This could be one of the following two: “_Open_” or “_Destroying_” + +## Getting Started + +### Unaltered Behaviors + +The Enjin Blockchain extends upon the implementation of the basic [staking pallet](https://crates.parity.io/pallet_staking/index.html) and [nomination pool pallet](https://wiki.polkadot.network/docs/learn-nomination-pools) to enable functionality such as Liquid Staking. Because it is an extension to an existing set of pallets, there are some queries and extrinsics that remain unaltered. + +1. [Listing All Pools](#listing-all-nomination-pools) + +2. [Listing a Single Pool](#listing-a-single-nomination-pool) + +3. [Listing Unbonding Members of a Pool](#listing-unbonding-members-of-a-nomination-pool) + +4. [Bonding into a Pool](#bonding-into-a-nomination-pool) + +5. [Unbonding from a Pool](#unbonding) + +6. [Withdrawing from a Pool](#withdrawing) + +7. [Nominating Validators for a Pool](#nominating-validators-for-a-nomination-pool) + +8. [Chilling a Pool](#chilling-a-nomination-pool) + +9. [Unbonding a Pool Deposit](#unbonding-1) + +10. [Withdrawing a Pool Deposit](#withdrawing-1) + +11. [Destroying a Pool](#destroying-a-nomination-pool) + +### Enjin Relaychain RPC Endpoints + +It is strongly recommended that you operate your own Enjin Relaychain node. You can read more about this in our documentation: [Running Relaychain Nodes](/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/02-run-relaychain-node.md) + +However, you may also use our official RPC endpoints: + +- `https://rpc.relay.blockchain.enjin.io` + +- `wss://rpc.relay.blockchain.enjin.io` + +Please note that our officially operated RPC endpoints are not archive nodes. This is non-essential to this integration guide unless you intentionally want to look at the historical state. + +### Polkadot.js API Library + +This Integration Guide uses the [@polkadot/api](https://github.com/polkadot-js/api) library, and assumes that the client has been defined in a variable named: api + +### About Degens + +[Degens](https://degens.io/) are a powerful NFT that are the backbone of staking on the Enjin Blockchain. These NFTs are essential to staking as they are a **requirement** for creating a Nomination Pool.  + +Each Degen can be assigned to a single Nomination Pool (this occurs during the Nomination Pool creation process) and then cannot be disassociated from the Nomination Pool unless it is destroyed. A Degen cannot be allocated to multiple Nomination Pools at the same time. Whoever holds the Degen is the “owner” of the Nomination Pool. + +Degens are built on-top of Enjin Blockchain’s incredibly powerful and versatile [multiToken pallet](/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md). + +### About sENJ and Liquid Staking + +sENJ is an . Due to it being a multi-token, this is what enables the concept of Liquid Staking. Liquid Staking, simply put, means that the sENJ is not locked to a specific account and it can in fact be used in processes such as governance or the [Stake Exchange](/04-enjin-blockchain/02-enjin-relaychain/02-stake-exchange-pallet.md). + +#### Difference Between ENJ and sENJ + +ENJ is the native token of the Enjin Blockchain. Whereas, sENJ is an created with the [multiToken pallet](/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md). When [bonding (staking)](/02-guides/02-blockchain/01-relaychain/04-joining-nomination-pools.md) ENJ to a nomination pool, sENJ is created at a 1:1 ratio. During the time that you are in the pool, holding sENJ, a Stake Factor accrues as the nomination pool accrues ENJ. This Stake Factor is a multiplier to the value of sENJ the user holds. This means that when the user then proceeds with unbonding, they in-turn can withdraw up to the amount factoring in the Stake Factor multiplier. + +The Stake Factor can decrease if the nominated Validators misbehave. Therefore, it is not possible to guarantee that the user would definitely receive, at minimum, their original ENJ balance or, at best, an amount greater than their original ENJ balance. + +## Integration Guide + +### Queries + +#### Listing All Nomination Pools + +**This query is unaltered from the original.** This query can be used to retrieve an exhaustive list of all Nomination Pools, along with details about each of the Nomination Pools. + +##### State Storage Query + +```javascript +nominationPools.bondedPools +``` + +##### Example Request + +```javascript +const pools = (await api.query.nominationPools.bondedPools.entries()) + .map(([account, data]) => [account.toHuman(), data]) + +console.log(Object.fromEntries(pools)) +``` + +##### Example Response + +```json +{ + "0": { + "state": "Open", + "commission": { + "current": null, + "max": null, + "changeRate": null, + "throttleFrom": null + }, + "tokenId": 1, + "capacity": "0x00000000001a784379d99db420000000", + "creationBlock": 1654079, + "name": "0x" + }, + ... +} +``` + +#### Listing a Single Nomination Pool + +**This query is unaltered from the original.** This query can be used to retrieve details about a specific Nomination Pool. + +##### State Storage Query + +```javascript +nominationPools.bondedPools(poolId : u32) +``` + +##### Example Request + +```javascript +const pool = await api.query.nominationPools.bondedPools(0) +console.log(pool) +``` + +##### Example Response + +```json +{ + "state": "Open", + "commission": + { + "current": null, + "max": null, + "changeRate": null, + "throttleFrom": null + }, + "tokenId": 1, + "capacity": "0x00000000001a784379d99db420000000", + "creationBlock": 1654079, + "name": "0x" +} +``` + +#### Listing Members of a Nomination Pool + +This query can be used to retrieve an exhaustive list of members who are currently staked into a specific Nomination Pool. + +##### State Storage Query + +```javascript +multiTokens.tokenAccounts( + collectionId : u128, // 1 for sENJ + tokenId : u128, // pool id + account : Option +) +``` + +##### Example Request + +```javascript +const pool_id = 0 +const members = (await api.query.multiTokens.tokenAccounts.entries(1, pool_id)) + .map(([account, data]) => [account.toHuman()[2], data]) + +console.log(Object.fromEntries(members)) +``` + +##### Example Response + +```json +{ + "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu": { + "balance": "0x00000000000007c1c839136681335ac3", + "reservedBalance": 0, + "lockedBalance": 0, + "namedReserves": {}, + "locks": {}, + "approvals": {}, + "isFrozen": false + }, + ... +} +``` + +#### Listing Unbonding Members of a Nomination Pool + +**This query is unaltered from the original.** This query can be used to retrieve an exhaustive list of members who are currently unbonding from a specific pool. + +##### State Storage Query + +```javascript +nominationPools.unbondingMembers( + poolId : u32, + account : Option +) +``` + +##### Example Request + +```javascript +const pool_id = 0 + +const unbondingMembers = (await api.query.nominationPools.unbondingMembers.entries(pool_id)) + .map(([account, data]) => [account.toHuman()[1], data]) + +console.log(Object.fromEntries(unbondingMembers)) +``` + +##### Example Response + +```json +{ + "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu": { + "unbondingEras": { + "249": "0x000000000000021e19e0c9bab2401979", + "260": "0x000000000000043c33c1937564803fb3", + "267": "0x000000000000021e19e0c9bab2400e34" + } + }, + ... +} +``` + +#### Reading a User’s ENJ Balance of a Nomination Pool + +This query can be used to read the _true_ ENJ Balance of an account for a specific Nomination Pool. There is a necessary distinction between reading the value of sENJ and the _true_ ENJ Balance that the user currently holds through the sENJ token. Refer to [Difference Between ENJ and sENJ](#difference-between-enj-and-senj) for more information. + +This query is more complicated as it requires first retrieving the Nomination Pool’s stash account, calculating the Stake Factor, retrieving the user’s sENJ balance in the Nomination Pool, and finally multiply their sENJ balance by the Stake Factor to derive their final balance. + +##### Example Request + +```javascript +import { bnToU8a, stringToU8a, u8aConcat, BN } from '@polkadot/util' + +const pool_id = 0 +const account = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" + +function getPoolStashAccount(api, pool_id) { + const pallet_id = api.consts.nominationPools.palletId.toU8a() + + return api.registry.createType('AccountId32', u8aConcat( + stringToU8a('modl'), + pallet_id, + new Uint8Array([1]), + bnToU8a(new BN(pool_id), { bitLength: 32, isLe: true }), + new Uint8Array(32) + )).toString() +} + +const pool_account = getPoolStashAccount(api, pool_id) +const pool_active_balance = (await api.query.staking.ledger(pool_account)).toJSON().active +const staked_enj_supply = (await api.query.multiTokens.tokens(1, pool_id)).toJSON().supply +const pool_stake_factor = pool_active_balance / staked_enj_supply + +const balance = (await api.query.multiTokens.tokenAccounts(1, pool_id, account)) + .toJSON().balance * pool_stake_factor + +console.log(balance) +``` + +Technically, the Stake Factor also considers the Nomination Pool’s reward account as part of the calculation. However, during each payout the funds are automatically reinvested into the Nomination Pool’s stash account, leaving the reward account empty (barring its existential deposit). + +##### Example Response + +``` +4.266220452892553e+22 +``` + +### Extrinsics + +#### Bonding into a Nomination Pool + +**This extrinsic is unaltered from the original.** This extrinsic can be called for the user to join a Nomination Pool and bond a given amount of ENJ into that Nomination Pool. + +##### Extrinsic + +```javascript +nominationPools.bond( + poolId : u32 (PoolId), + amount : PalletNominationPoolsBondValue (BondValueOf) +) +``` + +##### Example Request + +```javascript +const pool_id = 0 +const signer = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" + +// generate transaction to bond 100 ENJ +const tx = api.tx.nominationPools.bond(pool_id, {"amount": "100000000000000000000"}) + +// or generate transaction to bond all ENJ +// const tx = api.tx.nominationPools.bond(pool_id, {"fill": null}) + +// sign and send transaction +await tx.signAndSend(signer, ({ events = [], status, dispatchError }) => { /* handle */ }) +``` + +#### Unbonding and Withdrawing from a Nomination Pool + +**These extrinsics are unaltered from the original.** These extrinsics are permissionless only when the Pool State is _Destroying_. + +##### Unbonding + +This extrinsic can be used to initiate the unbonding process of a given amount of sENJ back to ENJ from a specific Nomination Pool. If the amount being unbonded is the complete amount of sENJ, the user is said to be leaving the Nomination pool. The unbonding process takes twenty-eight (28) eras. + +###### Extrinsic + +```javascript +nominationPools.unbond( + poolId : u32 (PoolId), + memberAccount : MultiAddress (AccountIdLookupOf), + unbondingPoints : Compact (BalanceOf) +) +``` + +###### Example Request + +```javascript +const pool_id = 0 +const account = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" +const amount = "100000000000000000000" // 100 ENJ + +// generate transaction +const tx = api.tx.nominationPools.unbond(pool_id, account, amount) + +// sign and send transaction +await tx.signAndSend(account, ({ events = [], status, dispatchError }) => { /* handle */ }) +``` + +##### Withdrawing + +This extrinsic can be used to withdraw sENJ that has successfully completed the unbonded period of twenty-eight (28) eras. The user will receive the amount unbonded in the form of ENJ. + +###### Extrinsic + +```javascript +nominationPools.withdrawUnbonded( + poolId : u32 (PoolId), + memberAccount : MultiAddress (AccountIdLookupOf), + numSlashingSpans : u32 +) +``` + +###### Example Request + +```javascript +const pool_id = 0 +const account = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" + +// generate transaction +const tx = api.tx.nominationPools.withdrawUnbonded(pool_id, account, 0) + +// sign and send transaction +await tx.signAndSend(account, ({ events = [], status, dispatchError }) => { /* handle */ }) +``` + +#### Creating a Nomination Pool + +This extrinsic is used to create a new Nomination Pool. The duration must be at least 30 eras, and must not exceed 1,000 eras. + +##### Requirements + +- User must own an Inactive Degen. + +- User must bond an 2,500 ENJ pool deposit. + +##### Extrinsic + +```javascript +nominationPools.create( + tokenId : u128 (TokenIdOf), + deposit : Compact (BalanceOf), + capacity : Compact (BalanceOf), + duration : Compact (EraIndex), + name : Bytes (PoolNameOf) +) +``` + +##### Example Request + +```javascript +const token_id = 1 // id of the Inactive Degen +const deposit = "2500000000000000000" // 2,500 ENJ - Pool Deposit +const capacity = "100000000000000000000000" // 100,000 ENJ +const duration = 100 // 100 eras +const name = "Demo Pool" +const signer = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" + +// generate transaction +const tx = api.tx.nominationPools.create( + token_id, + deposit, + capacity, + duration, + name +) + +// sign and send transaction +await tx.signAndSend(signer, ({ events = [], status, dispatchError }) => { /* handle */ }) +``` + +#### Mutating a Nomination Pool + +This extrinsic is used to update (mutate) an existing Nomination Pool. + +##### Requirements + +- User must be the Pool Owner. + +##### Extrinsic + +```javascript +nominationPools.mutate( + poolId : u32 (PoolId), + mutation : PalletNominationPoolsPoolMutation (PoolMutationOf) +) +``` + +##### Example Request + +```javascript +const pool_id = 0 +const signer = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" + +// generate transaction +const tx = api.tx.nominationPools.mutate( + pool_id, + { + duration: "30", // 30 days (optional) + newCommission: { // (optional) + "SomeMutation": "50000000" // 5% (optional) + }, + maxCommission: "90000000", // 9% (optional) + changeRate: { + maxDelta: "5000000", // 0.5% (optional) + minDelay: "15", // 15 days (optional) + }, + capacity: "100000000000000000000000", // 100,000 ENJ (optional) + name: "Demo Pool" (optional) + } +) + +// sign and send transaction +await tx.signAndSend(signer, ({ events = [], status, dispatchError }) => { /* handle */ }) +``` + +#### Nominating Validators for a Nomination Pool + +**This extrinsic is unaltered from the original.** This extrinsic is used to modify which Validators the Nomination Pool has nominated. It is also used to unchill a Nomination Pool. + +##### Requirements + +- User must be the Pool Owner. + +##### Extrinsic + +```javascript +nominationPools.nominate( + poolId : u32 (PoolId), + validators : Vec (Vec) +) +``` + +##### Example Request + +```javascript +const pool_id = 0 +const signer = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" +const validators = [ + "enG2tPZeAkKCNSxgXnWNDhGf5j83GZboj41ypUENcGZCNDheN", + "enBRFocgo4aPAVtny93ZLaTPX9JpbxrSeBt29uXdLy3AcG8Dx", + // ... +] + +// generate transaction +const tx = api.tx.nominationPools.nominate(pool_id, validators) + +// sign and send transaction +await tx.signAndSend(signer, ({ events = [], status, dispatchError }) => { /* handle */ }) +``` + +#### Chilling a Nomination Pool + +**This extrinsic is unaltered from the original.** This extrinsic is used to chill the Nomination Pool — a chilled Nomination Pool refers to one that does not actively contribute its stake to the Validator selection process. Chilling the Nomination Pool takes effect in the next era. + +##### Requirements + +- User must be the Pool Owner. + +##### Extrinsic + +```javascript +nominationPools.chill(poolId : u32 (PoolId)) +``` + +##### Example Request + +```javascript +const pool_id = 0 +const signer = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" + +// generate transaction +const tx = api.tx.nominationPools.chill(pool_id) + +// sign and send transaction +await tx.signAndSend(signer, ({ events = [], status, dispatchError }) => { /* handle */ }) +``` + +#### Destroying a Nomination Pool + +**This extrinsic is unaltered from the original.** This sets the Pool State to _Destroying_. + +:::danger +This process cannot be reversed! +::: + +##### Requirements + +- User must be the Pool Owner. + +##### Extrinsic + +```javascript +nominationPools.destroy(poolId : u32 (PoolId)) +``` + +##### Example Request + +```javascript +const pool_id = 0 +const signer = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" + +// generate transaction +const tx = api.tx.nominationPools.destroy(pool_id) + +// sign and send transaction +await tx.signAndSend(signer, ({ events = [], status, dispatchError }) => { /* handle */ }) +``` + +#### Unbonding and Withdrawing a Pool Deposit from a Nomination Pool + +**These extrinsics are unaltered from the original.** These extrinsics are permissionless. + +##### Unbonding + +This extrinsic will initiate a twenty-eight (28) era unbonding period. + +###### Requirements + +- The Pool State must be _Destroying_. + +- There must be no remaining Pool Members for the Nomination Pool being destroyed. + +##### Extrinsic + +```javascript +nominationPools.unbondDeposit(poolId : u32 (PoolId)) +``` + +##### Example Request + +```javascript +const pool_id = 0 +const signer = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" + +// generate transaction +const tx = api.tx.nominationPools.unbondDeposit(pool_id) + +// sign and send transaction +await tx.signAndSend(signer, ({ events = [], status, dispatchError }) => { /* handle */ }) +``` + +##### Withdrawing + +This extrinsic will withdraw the Pool Deposit which will subsequently destroy the Nomination Pool. The Degen assigned to the Nomination Pool will become an Inactive Degen and will **not** be destroyed alongside the Nomination Pool. + +###### Requirements + +- The Pool Deposit unbonding period (twenty-eight (28) eras)  must have passed. + +###### Extrinsic + +```javascript +nominationPools.withdrawDeposit(poolId : u32 (PoolId)) +``` + +###### Example Request + +```javascript +const pool_id = 0 +const signer = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" + +// generate transaction +const tx = api.tx.nominationPools.withdrawDeposit(pool_id) + +// sign and send transaction +await tx.signAndSend(signer, ({ events = [], status, dispatchError }) => { /* handle */ }) +``` + +## Data Indexing + +It is recommended to index data pertaining to Staking and Nomination Pools as this enables a more performant and richer experience. However, it is **not essential** for a basic integration, as illustrated through this Integration Guide. + +Indexing this data enables you to monitor the change over time and infer the increase or decrease in sENJ holdings over time. This enables your integration to visually show payouts and their associated amounts per Pool Member. + +## Extra Resources / Further Reading + +- [Subscan](https://www.subscan.io/), a Blockchain Explorer, has useful pages on [Validators](https://enjin.subscan.io/validator) and [Nomination Pools](https://enjin.subscan.io/nomination_pool). + +- [Enjin Console](https://console.enjin.io/) has a dedicated page for [Staking](https://console.enjin.io/?rpc=wss%3A%2F%2Frpc.relay.blockchain.enjin.io#/staking) and [Nomination Pools](https://console.enjin.io/?rpc=wss%3A%2F%2Frpc.relay.blockchain.enjin.io#/staking/pools). + +- For more in-depth information, feel free to explore other topics within the [Enjin Relaychain](/04-enjin-blockchain/02-enjin-relaychain/02-enjin-relaychain.md) section. diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/03-creating-nomination-pools.md b/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/03-creating-nomination-pools.md new file mode 100644 index 0000000..03b7106 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/03-creating-nomination-pools.md @@ -0,0 +1,91 @@ +--- +title: "Creating Nomination Pools" +slug: "creating-nomination-pools" +--- +## Creating nomination pools + +Enjin Relaychain uses Nominated Proof of Stake (NPoS) to secure the network. Nominators can select which validators to delegate the stake to. A nomination pool acts as a single nominator and lowers the entry barrier for the user to participate in NPoS. + +## Degen NFT + +To create a nomination pool, an account must hold a Degen NFT. + +A Degen NFT is a special token that is used to create the pool and the holder might receive pool commissions, the collection for all Degen NFTs has a fixed ID of `2`. + +![](/img/components/enjin-relaychain/9.png) + +## Create a nomination pool (RAW extrinsic) + +Once you hold a Degen NFT, you can create a nomination pool using `nominationPools.create()`. + +![](/img/components/enjin-relaychain/10.png) + +## Create a nomination pool (Enjin Console) + +[Enjin Console](https://console.enjin.io/) has custom UI that simplifies this process, its located under `Network -> Staking -> Pools` + +![](/img/components/enjin-relaychain/11.png) + +This page allows to create a nomination pool, see an overview, and manage its settings. Let's create a new pool and fill out the form. + +Important to keep in mind: + +- Creating a pool requires a Degen NFT (`tokenId` must be provided) +- Minimum initial stake deposit is required to create a pool (`2500 ENJ`) +- `Duration` must be at least `30 eras` (30 days) and not more than `1000 eras` (1000 days) + +## Creation parameters + +It's worth noting that `tokenId` is the ID of the Degen NFT you hold. `PoolId` may or may not match the `tokenId`, this wont change anything, but is worth keeping in mind. `Duration` is defined in `eras`, where `1 era = 24 hours` on Enjin Relaychain, and minimum value is set at `30 eras`. `Capacity` is the maximum amount of [sENJ](/04-enjin-blockchain/02-enjin-relaychain/01-validator-staking.md#tokens-overview) that can be staked in the pool. The maximum Capacity is determined by the Degen's token `max_pool_capacity` attribute. +Note that this is sENJ, not ENJ. To learn more about the difference, [check out the Validator Staking page](/04-enjin-blockchain/02-enjin-relaychain/01-validator-staking.md). + +![](/img/components/enjin-relaychain/12.png) + +## Pool created + +Now the nomination pool is created and the initial stake is deposited. You can also notice that the only member is the Pool stash account. + +![](/img/components/enjin-relaychain/13.png) + +## Join a nomination pool + +Now you can also join the pool, and provide your own stake. + +![](/img/components/enjin-relaychain/14.png) + +## Pool overview + +Once you have joined the pool, you can check the overview of the pool, and the members list should be updated. + +The pool is currently not nominating any validators and is not earning any rewards. + +![](/img/components/enjin-relaychain/15.png) + +## Nominate validators + +This step is very crucial to the whole operation. The rewards will start to accrue when the pool is nominating validators that are selected to produce blocks. To select nominators, use the `Nominate` button that is located under each expanded pool. + +![](/img/components/enjin-relaychain/16.png) + +At this point we have nominated both validators and should just wait to see the rewards. Once some rewards have accrued and an era has passed, the `Payout rewards` button can be used to distribute rewards to everyone in the pool. Rewards are paid out once per validator per era. + +![](/img/components/enjin-relaychain/17.png) + +## Mutating nomination pool + +As the pool owner, you can change the settings of the pool. This includes changing the duration, commission, and management roles of the pool. To do this, use the `Mutate` button that is located under each expanded pool. + +![](/img/components/enjin-relaychain/18.png) + +## Destroying nomination pool + +Destroying a nomination pool involves a series of steps. + +:::tip +For a more convenient way to destroy a pool, consider using [NFT.io](https://nft.io). +::: + +To manually destroy a pool using the Enjin Console, follow these steps: +1. **Change the Pool State to Destroying**: Initiate the destruction process by setting the pool's state to `Destroying`. You can do this by calling the `nominationPools.destroy()` extrinsic. +2. **Unbond Member Stakes**: If any members have staked funds in the pool, their stakes must be unbonded. Once the pool is in the `Destroying` state, the `nominationPools.unbond()` extrinsic becomes permissionless. This means the pool owner can initiate unbonding for all members by calling `nominationPools.unbond()` on their behalf. After the unbonding period for all members ends, their stakes must be withdrawn using the `nominationPools.withdrawUnbonded()` extrinsic, which is also permissionless at this stage. +3. **Unbond and Withdraw the Initial Deposit**: Once the pool is in the `Destroying` state and has no bonded members, the pool owner must unbond their initial ENJ stake deposit. Do this by calling the `nominationPools.unbondDeposit()` extrinsic. After the unbonding period for the deposit ends, use the `nominationPools.withdrawDeposit()` extrinsic to withdraw it and finalize the pool's destruction. \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/04-joining-nomination-pools.md b/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/04-joining-nomination-pools.md new file mode 100644 index 0000000..41c8557 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/04-joining-nomination-pools.md @@ -0,0 +1,85 @@ +--- +title: "Joining Nomination Pools" +slug: "joining-nomination-pools" +--- +:::info The Enjin Blockchain Console +Use [console.enjin.io](https://console.enjin.io/) to use the user interface referenced in this document. +::: + +:::tip +For a more user friendly staking interface, we suggest to use the [Enjin Wallet](https://enjin.io/products/wallet). +::: + +Enjin Relaychain operates using Nominated Proof of Stake (NPoS) consensus algorithm. NPoS is a variation of Proof of Stake (PoS) consensus algorithm, where the right to produce blocks is delegated to a set of validators. Validators are selected by nominators, who delegate their stake to validators. Validators are rewarded for producing blocks and are sharing the rewards with their nominators. + +## Find a nomination pool that satisfies your needs + +![](/img/components/enjin-relaychain/20.png) + +You should look out for the following criteria when choosing a nomination pool: + +- **Points -** Points display the amount of staked ENJ (sENJ) this pool has in circulation. Its important to remember that each pool issues its own unique `sENJ-poolID` token, so the amount of points does not directly correlate to the amount of ENJ staked in the pool. +- **Capacity -** shows the maximum amount of sENJ that the pool can issue. If the capacity is reached, you will not be able to stake ENJ in the pool. +- **Nominees -** displays the validators that the pool is nominating. Remember that the pool will only receive rewards if their nominated validators are selected to produce a block. If the validator is slashed for any reason (e.g. being offline for too long), the funds nominated by pool will be slashed as well. +- **Members -** shows the current participants for each nomination pool. More members can provide higher stake, but it also means that rewards will be split between more people. + +![](/img/components/enjin-relaychain/21.png) + +## Stake ENJ in a nomination pool + +The pool with `ID 0` is nominating `Alice_STASH` validator and has 3 pool members (including the pool account). To stake 100 ENJ in this pool, click on the `Join` button. + +![](/img/components/enjin-relaychain/22.png) + +Let's say Dave wishes to join this pool with 100 ENJ. With current staked ENJ and issued sENJ, he should receive 100 points (sENJ) for his stake. + +![](/img/components/enjin-relaychain/23.png) + +Now we can also check our `sENJ-poolID` balance to be sure our stake was successful: + +![](/img/components/enjin-relaychain/24.png) + +This will return Dave's `sENJ-poolID` balance for this specific pool with ID 0, which is also the sENJ token id. And this returns a balance of 100 sENJ units. + +![](/img/components/enjin-relaychain/25.png) + +### Rewards + +Rewards accrue every era (24h) for validators and subsequently to nominators. They can be claimed using n`ominationPools.payoutRewards()` extrinsic, which is easily accessible under each pool when expanded. This can be called by any member of the pool and will distribute rewards to all members of the pool including validators. + +![](/img/components/enjin-relaychain/26.png) + +### Inflation + +Every era the rewards are generated for NPoS participants, creating inflation. The inflation rate is fixed at `5% per year`. So every era, `~0.0134%` of the total ENJ supply on chain is generated and distributed to NPoS participants. Calculation takes compounding effect into consideration. The rewards are distributed proportionally to the amount of stake each participant has in the network. + +![](/img/components/enjin-relaychain/27.png) + +## Leaving a nomination pool + +To leave a nomination pool, expand the pool details and click `Unbond`. +:::note +You must wait 28 eras for the unbonding period to complete. Once finished, click `Withdraw unbonded` to retrieve your funds. +::: + +![](/img/components/enjin-relaychain/28.png) + +We can see that correct events were emitted, which also indicates that rewards were paid out to all members of the pool and now the same 100 sENJ are worth more than 100 ENJ, and Dave unbonds `100.608836621592543849 ENJ`: + +![](/img/components/enjin-relaychain/29.png) + +Unbonded funds can only be withdrawn once the correct era is reached. We can observe how much and when each unbonding member can withdraw by looking at the chain state + +![](/img/components/enjin-relaychain/30.png) + +This informs us that at era 118, we will be able to withdraw our unbonded ENJ from the pool (including reward). We can see current era at `staking.currentEra` storage. + +![](/img/components/enjin-relaychain/31.png) + +And now that we are past the unbonding era, we can withdraw our funds: + +![](/img/components/enjin-relaychain/32.png) + +And we can see that the correct amount of ENJ was withdrawn from the pool and transferred to us (Dave): + +![](/img/components/enjin-relaychain/33.png) diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/_category_.json b/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/_category_.json new file mode 100644 index 0000000..6d49583 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Relaychain", + "link": { + "type": "generated-index", + "description": "Explore guides on integrating staking, creating, and joining nomination pools on the Enjin Relay Chain." + } +} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-enjin-blockchain-nodes.md b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-enjin-blockchain-nodes.md new file mode 100644 index 0000000..b4865bd --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-enjin-blockchain-nodes.md @@ -0,0 +1,54 @@ +--- +title: "Enjin Blockchain Nodes" +slug: "../about-enjin-blockchain-nodes" +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +## Types of Nodes + +Enjin supports three types of nodes, RPC, collator, and archive. Choosing the type of node that best suits your use case is crucial since each type has specific requirements and uses. The good news is that you only need to compile the node once, and then you can run it as an RPC node, collator, or archive node. + +### RPC Node + +An RPC node is a type of node that can connect to the Enjin network and participate in it. Typically, it is hosted on a remote server and accessed by client-side applications as an RPC or WS endpoint, serving as a connection interface to the Matrixchain. However, RPC nodes do not store the entire history of the chain, meaning they cannot provide historical data. It is important to consider this limitation when choosing an RPC node for your use case. + +### Collator Node + +A collator node is responsible for maintaining a Matrixchain by collecting transactions from users and producing state transition proofs for Relaychain validators. Collators also maintain a full node for the Relaychain and a full node for their particular Matrixchain. This means they retain all the necessary information to author new blocks and execute transactions the same way as validators do on current PoS blockchains. Typically, collators will collate and execute transactions to create a block, then broadcast it to other collators and Relaychain validators on the network. + +:::warning +Currently, the functionality of running a collator node and the associated rewards system is not available. We will be sure to provide updates as soon as these features become available. +::: + +### Archive Node + +An archive node stores the complete history of the network and is typically used by developers for testing applications and running analytics. It is also used by blockchain explorers to enable users to explore the entire state of the chain, as well as by collators to verify the network's state. However, running an archive node requires significantly more disk space compared to other node types. + +## Network Participation + +Users can earn rewards by running a collator node to participate in the Enjin network. However, it's essential to understand that collators do not secure the network. In Enjin, PoA (Proof of Authority) consensus is used, and block producers or collators are chosen from the list of authorized validators. More collators do not necessarily mean a better or more secure network since Relaychain validators will reject an invalid Matrixchain block. +Having too many collators can also slow down the network. The only power that collators possess is transaction censorship. To avoid censorship, a Matrixchain must have some neutral collators, but not necessarily a majority. In theory, the censorship problem can be solved by having only one honest collator. + +## Staking + +The management of collators on the Enjin Matrixchain is handled by the collator staking pallet. This pallet selects and rewards block producers for each round. Two types of rewards are generated for every block created, which are collected from transaction tips and fees. These fees are then transferred to the fee distribution account at the end of every block. The accumulated fees are then distributed at the end of every session by the Pools pallet to registered pools. Currently, the collator receives a 70% share from the fee distribution. The collator staking pallet then distributes these rewards to collators at the end of each session using the funds received in the collator pool account. + +## Rewards + +The formula for calculating the block reward is: +reward = `blocks_authored_by_collator \* reward_per_block` +where `reward_per_block = total_reward_per_session / total_blocks_in_session` +The rewards are then distributed depending on the type of collator: + +- Invulnerable Collators: the fees are transferred back to the fee distribution account +- Other Collators: fees are transferred to collator accounts + +## Collator selection + +Collators are selected for each session based on the following criteria: + +- Invulnerable nodes are always selected as collators +- The remaining number of collators (`MaxCollators - Invulnerables`) is filled with the top candidates ordered by total stake (Collator's own funds and nominators' funds). + +Invulnerable nodes can only be set by governance or . diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-running-a-node.md b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-running-a-node.md new file mode 100644 index 0000000..4f86345 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-running-a-node.md @@ -0,0 +1,67 @@ +--- +title: "Hosting an Enjin Node" +slug: "running-a-node" +description: "Running an Enjin node allows you to connect to the Enjin network, expose the RPC endpoint, and author blocks. This guide walks you through the process of running your own node locally and on a remote server." +--- +## Requirements + +In general, requirements for running an Enjin node are quite similar to running a Substrate node. + +Therefore, if you are not sure about specific details, you can always refer to the [official Substrate documentation](https://substrate.dev/docs/en/knowledgebase/getting-started/). However, building a Matrixchain Node, i.e. Enjin Matrixchain node, requires a little more time and resources, since Matrixchain nodes also run a full Relaychain node. + +### Hardware + +For building and running an Enjin node, the recommended hardware is: + +| Component | Requirement | +| :--------- | :-------------------- | +| OS | Ubuntu 20.04 or newer | +| CPU | 4 cores | +| RAM | 16 GB | +| Disk Space | 256 GB SSD | + +:::tip +It is recommended to have more disk space available (512 GB+) if you are running an Archive Node. +::: + +### Ports + +This table shows the default ports that are used by the Enjin Matrixchain node and embedded Relaychain node: + +| Description | Ingress Port | Egress Port | +| :-------------- | :------------------- | :------------------------ | +| P2P | **TCP** 30333, 30334 | **TCP** 443, 30333, 30334 | +| RPC (HTTP & WS) | **TCP** 9933 | - | +| Prometheus | **TCP** 9615 | - | + +This table shows the ports that are recommended to be exposed depending on the type of node you are running: + +| Description | Archive / RPC | Collator / Validator | +| :-------------- | :------------ | :------------------- | +| P2P | **required** | **required** | +| RPC (HTTP & WS) | ✅ | ❌ | +| Prometheus | ✅ | ✅ | + +## Advanced + +This section covers advanced topics associated with running the Enjin node. + +### Node Key + +Substrate nodes use node keys for identification in peer-to-peer communication. To generate a node key, use the [subkey tool](https://docs.substrate.io/reference/command-line-tools/subkey/), which can be downloaded using the following command: + +`cargo install --force --git https://github.com/paritytech/substrate subkey` + +After installing the [subkey tool](https://docs.substrate.io/reference/command-line-tools/subkey/), you can generate the node key using the following command: + +`subkey generate-node-key --file ./node.key # or any other path` + +### Purging the chain + +To begin anew, without any chain history, you may want to purge the chain. + +:::danger +The following will command will purge the chain data for the Enjin Matrixchain +::: + +`./target/release/matrixchain purge-chain --chain mainnet` diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/02-run-relaychain-node.md b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/02-run-relaychain-node.md new file mode 100644 index 0000000..067d4bd --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/02-run-relaychain-node.md @@ -0,0 +1,128 @@ +--- +title: "Run a Relaychain Node" +slug: "run-relaychain-node" +--- +There are two different ways in which you can a Node, the first one being through Docker, and the second one through a Binary (either precompiled or [Built from Source](/enjin-blockchain-nodes/building-from-source)). + +## Create Data Directory + +First begin by creating a data directory for the: `sudo mkdir /data` + +Then set the permission for it using `sudo chown -R 1000:1000 /data` + +## Docker + +:::info +The Docker Image can be found on Docker Hub at: [enjin/relaychain](https://hub.docker.com/r/enjin/relaychain). +In this example, we demonstrate using the version `latest`. However, in practice, we recommend statically setting this to a specific version (such as `v100`) and then performing manual upgrades to your nodes as and when appropriate. This is to ensure that your node doesn't inadvertently differ from one restart to the next. See the [Upgrading using Docker](#upgrading-using-docker) section for more information. +::: + +You can use the following `docker-compose.yml` file: + +```yaml +services: + relaychain: + container_name: relaychain + image: enjin/relaychain:latest + ports: + - 9944:9944 + - 9615:9615 + - 30333:30333 + - 30334:30334 + volumes: + - /data:/chainstate + command: [ + "--name=enjin-relay-docker", + "--rpc-external", + "--rpc-cors=all", + "--chain=enjin", + "--base-path=/chainstate" + ] +``` + +Simply run the command `docker-compose up -d` to run the container. + +You can use the command `docker ps` to check that the container is running. You can then stream the logs (by taking the ``) to check its current sync position by using: `docker logs -f --since 1m` + +## Binary + +### Command + +`$ ./enjin --name "enjin-relay-docker" --rpc-external --chain enjin` + +Connecting to your node + +Depending on the use case, there are a couple of ways to connect to the node: + +- WebSocket Connection: `ws://localhost:9944` +- RPC (HTTP) Connection: `http://localhost:9944` + +## Archive Node + +In order to run an archive node, the following argument needs to be passed to either the binary or added to the command section of the `docker-compose.yml` file: + +`--pruning=archive` + +This should be appended after line 19 in the `docker-compose.yml` file, or at the end of the Binary execution command. + +## Upgrading + +:::info It is not always possible to downgrade a node. +In the event the node incurred a database upgrade, it is no longer possible to downgrade the node to an older version without first re-syncing the node. + +Additionally, in the event an on-chain upgrade has been enacted that requires a newer node minimum version, it will not be possible to downgrade below that version as new blocks following the upgrade will fail to import. +**Always keep your node version up-to-date.** +::: + +In order to ensure compatibility with the chain at all times, and to ensure the best security, it is imperative that node operators keep their nodes up-to-date with each release that is published. The process of upgrading is incredibly simple, and in almost all cases, requires very little involvement by the node operator. This is because, the node will automatically detect when it's using an older version of the database and automatically upgrade it to the latest one that is compatible with the + +We recommend that node operators subscribe to our mailing list [mailing-list-node-operators@enjin.io](https://groups.google.com/a/enjin.io/g/mailing-list-node-operators) in order to be informed when we publish a new node version. However, for those who don't want to subscribe, you can query our [Docker Hub repository](https://hub.docker.com/r/enjin/relaychain/tags) to check for new versions. All versions are automatically pushed to Docker Hub, so you will always be able to find the latest version there. + +### Upgrading using Docker + +Open your `docker-compose.yml` in a text editor and locate the `image` line, in the below example it is line 4: + +```yaml +services: + relaychain: + container_name: relaychain + image: enjin/relaychain:latest # <-- this line + ports: + - ... +``` + +If you are using the version `latest` (as in the above example), simply restarting that node at any point will ensure you're automatically updated to the very latest stable version. However, if you have statically set an image version, simply alter the line to specify the latest versioned release that appears on [Docker Hub](https://hub.docker.com/r/enjin/relaychain/tags). + +For example, if you are running `enjin/relaychain:v100` and the latest version is `enjin/relaychain:v200` you would simply need to make the following change: + +```yaml +services: + relaychain: + container_name: relaychain ++ image: enjin/relaychain:v200 # add this line +- image: enjin/relaychain:v100 # remove this line + ports: + - ... +``` + +Once updated, simply restart the node by first stopping the existing node and then repeating the steps to start the node using the command `docker-compose up -d` in the directory where the `docker-compose.yml`file is located. + +### Upgrading using Binary + +Simply download the latest binary and run it as you have always done as per the [Binary, Command](/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/02-run-relaychain-node.md#command) section. It's as simple as that! + +#### Upgrading from Source + +If you are upgrading based on a build you've produced from the source code, you must navigate to the directory containing the source code in a terminal. Once you have built the binary, simply follow the instruction [Upgrading using Binary](/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/02-run-relaychain-node.md#upgrading-using-binary) to complete the upgrade. + +##### Git-cloned Repository + +You should then fetch the latest changes using `git fetch` and then retrieve the source code of the latest tagged version `git checkout `. Once you have swapped to the latest source code for that release, you can simply run `cargo build --release` (as guided in [Building From Source](/enjin-blockchain-nodes/building-from-source)) to produce a new binary using the latest version. + +##### Downloaded Repository + +In the event that the repository was directly downloaded, and not cloned via `git`, you will first need to download the source code for the latest version. Once done, extract the source code and navigate to the directory in a terminal. You can then run `cargo build --release` (as guided in [Building From Source](/enjin-blockchain-nodes/building-from-source)) to produce a new binary. + +## Looking to run a validator? + +See the [Running a Validator](/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md) section. diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/03-run-matrixchain-node.md b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/03-run-matrixchain-node.md new file mode 100644 index 0000000..c4dc4a5 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/03-run-matrixchain-node.md @@ -0,0 +1,128 @@ +--- +title: "Run a Matrixchain Node" +slug: "run-matrixchain-node" +--- +There are two different ways in which you can a Node, the first one being through Docker, and the second one through a Binary (either precompiled or [Built from Source](/enjin-blockchain-nodes/building-from-source)). + +:::warning Currently, it is not possible to run a Enjin Matrixchain collator node. +This guide will be updated once that option is available. +::: + +## Docker + +:::info +The Docker Image can be found on Docker Hub at: [enjin/matrixchain](https://hub.docker.com/r/enjin/matrixchain) +In this example, we demonstrate using the version `latest`. However, in practice, we recommend statically setting this to a specific version (such as `v100`) and then performing manual upgrades to your nodes as and when appropriate. This is to ensure that your node doesn't inadvertently differ from one restart to the next. See the [Upgrading using Docker](#upgrading-using-docker) section for more information. +::: + +You can use the following `docker-compose.yml` file: + +```yaml +services: + matrixchain: + container_name: matrxichain + image: enjin/matrixchain:latest + ports: + - 9944:9944 + - 9615:9615 + - 30333:30333 + - 30334:30334 + volumes: + - /MY/LOCAL/DIRECTORY:/chainstate + command: [ + "--name=enjin-matrix-docker", + "--rpc-external", + "--rpc-cors=all", + "--chain=mainnet", + "--base-path=/chainstate", + "--", + "--name=enjin-matrix-docker", + "--port=30334", + "--chain=enjin", + "--base-path=/chainstate" + ] +``` + +Simply run the command `docker-compose up -d` to run the container. + +## Binary + +### Command + +```bash +$ ./matrix --name "enjin-matrix-docker" --rpc-external --chain mainnet + -- --name="enjin-matrix-docker" --port 30334 --chain enjin +``` + +### Connecting to your node + +Depending on the use case, there are a couple of ways to connect to the node: + +- WebSocket Connection: `ws://localhost:9944` +- HTTP Connection: `http://localhost:9944` + +## Archive Node + +In order to run an archive node, the following argument needs to be passed to either the binary or added to the command section of the `docker-compose.yml` file: + +`--pruning archive` + +These two arguments should be appended after line 19 in the `docker-compose.yml` file, or just before the backslash (`\`) on line 1 (end of line) of the Binary execution command. + +## Upgrading + +:::warning It is not always possible to downgrade a node. +In the event the node incurred a database upgrade, it is no longer possible to downgrade the node to an older version without first re-syncing the node. + +Additionally, in the event an on-chain upgrade has been enacted that requires a newer node minimum version, it will not be possible to downgrade below that version as new blocks following the upgrade will fail to import. +**Always keep your node version up-to-date.** +::: + +In order to ensure compatibility with the chain at all times, and to ensure the best security, it is imperative that node operators keep their nodes up-to-date with each release that is published. The process of upgrading is incredibly simple, and in almost all cases, requires very little involvement by the node operator. This is because, the node will automatically detect when it's using an older version of the database and automatically upgrade it to the latest one that is compatible with the + +We recommend that node operators subscribe to our mailing list [mailing-list-node-operators@enjin.io](https://groups.google.com/a/enjin.io/g/mailing-list-node-operators) in order to be informed when we publish a new node version. However, for those who don't want to subscribe, you can query our [Docker Hub repository](https://hub.docker.com/r/enjin/matrixchain/tags) to check for new versions. All versions are automatically pushed to Docker Hub, so you will always be able to find the latest version there. + +### Upgrading using Docker + +Open your `docker-compose.yml` in a text editor and locate the `image` line, in the below example it is line 4: + +```yaml +services: + matrixchain: + container_name: matrixchain + image: enjin/matrixchain:latest # <-- this line + ports: + - ... +``` + +If you are using the version `latest` (as in the above example), simply restarting that node at any point will ensure you're automatically updated to the very latest stable version. However, if you have statically set an image version, simply alter the line to specify the latest versioned release that appears on [Docker Hub](https://hub.docker.com/r/enjin/matrixchain/tags). + +For example, if you are running `enjin/matrixchain:v100` and the latest version is `enjin/matrixchain:v200` you would simply need to make the following change: + +```yaml +services: + matrixchain: + container_name: matrixchain ++ image: enjin/matrixchain:v200 # add this line +- image: enjin/matrixchain:v100 # remove this line + ports: + - ... +``` + +Once updated, simply restart the node by first stopping the existing node and then repeating the steps to start the node using the command `docker-compose up -d` in the directory where the `docker-compose.yml`file is located. + +### Upgrading using Binary + +Simply download the latest binary and run it as you have always done as per the [Binary, Command](#command) section. It's as simple as that! + +#### Upgrading from Source + +If you are upgrading based on a build you've produced from the source code, you must navigate to the directory containing the source code in a terminal. Once you have built the binary, simply follow the instruction [Upgrading using Binary](#upgrading-using-binary) to complete the upgrade. + +##### Git-cloned Repository + +You should then fetch the latest changes using `git fetch` and then retrieve the source code of the latest tagged version `git checkout `. Once you have swapped to the latest source code for that release, you can simply run `cargo build --release` (as guided in [Building From Source](/enjin-blockchain-nodes/building-from-source)) to produce a new binary using the latest version. + +##### Downloaded Repository + +In the event that the repository was directly downloaded, and not cloned via `git`, you will first need to download the source code for the latest version. Once done, extract the source code and navigate to the directory in a terminal. You can then run `cargo build --release` (as guided in [Building From Source](/enjin-blockchain-nodes/building-from-source)) to produce a new binary. diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/_category_.json b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/_category_.json new file mode 100644 index 0000000..5d8916e --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Enjin Blockchain Nodes" +} + \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md new file mode 100644 index 0000000..210d0b7 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md @@ -0,0 +1,210 @@ +--- +title: "Running a Validator" +slug: "run-a-validator" +--- +## Preliminaries​ + +Operating a Validator carries significant responsibility. You're responsible for both your stake and that of your nominators. Errors can lead to a loss of tokens and harm your reputation. Despite this, being a Validator can be fulfilling as you help secure a decentralized network and increase your holdings. + +:::warning +You should have strong system administration skills before trying to run your own Validator. You'll need to address technical problems with your node independently. Being a Validator is more than just running the Enjin Relaychain binary. +::: + +### How much ENJ do I need to become an Active Validator? + +There's a distinction between becoming a Validator, and becoming an Active Validator. In order to become a Validator, you need to bond a minimum of 15,000 ENJ. However, becoming an Active Validator requires further economical backing. There is currently a maximum of 13 Active Validators, and so your total stake must be within the top 13. The total stake of a Validator consists of the Validator's own stake, along with the collective stake of all nominators that nominate it. Outside of the top 13, your Validator will remain waiting. + +The simplest way to estimate the required stake would be to navigate to the `Staking` \> `Targets` page on the Enjin Console and read the value of "Lowest Staked" and staking an additional 1 ENJ on-top of that value. + +:::note +The minimum backing required to become an Active Validator on Enjin Relaychain, as of era 659, is 32.5 million ENJ. +::: + +:::danger Operating an insecure or improper setup may result in loss of ENJ! +It is important to acknowledge that any ENJ that you stake for your Validator is liable to being slashed. If you are not confident in your ability to run a Validator, it is recommended to nominate your ENJ to a trusted Validator instead. +::: + +## Initial Set-up + +### Requirements + +To run a Validator, most people use a cloud server with Linux. It's recommended to use a recent version of Debian Linux. This guide uses `Ubuntu 22.04 LTS`, but steps should be similar for other operating systems. + +### Reference Hardware + +Enjin Relaychain's transaction weights are based on specific hardware benchmarks. These were tested on Amazon's `m6id.xlarge`. Validators should use hardware that matches or exceeds these benchmarks to process blocks efficiently. + +- **CPU** + - x86_64 architecture + - 4-cores @ 3.4 GHz + - Intel Ice Lake (or newer); or AMD Zen3 (or newer) +- **Storage** + - 192 GB NVMe SSD: it is important to note that, overtime, the size of the Enjin Blockchain, and its ecosystem, will grow and therefore scaling of the storage is imperative. +- **Memory** + - 16 GB DDR4 ECC + +:::tip +It is not necessary to use an excessive number of CPU cores as operating a node is a single-threaded operation. Therefore, using a higher clocked CPU with 4-cores is more than sufficient. +::: + +The above hardware specifications are not the minimum specifications. However, they should be considered as best practice. Operating a Validator requires the use of professional hardware. Using inferior hardware can lead to performance problems, fewer era points, and even potential slashing. + +### Install a Network Time Protocol (NTP) Client + +[NTP](https://en.wikipedia.org/wiki/Network_Time_Protocol) is a networking protocol designed to synchronize the clocks of computers over a network. NTP allows you to synchronize the clocks of all the systems within the network. Currently it is required that the local clocks of Validators stay reasonably in-sync. Therefore, you should be running a NTP. Most modern Linux installations include an NTP client installed out-of-the-box for time synchronisation. + +#### Checking System Clock Synchronization + +You can check whether you currently have a NTP client installed by running the following command: + +`timedatectl` + +In the event that you have a NTP client installed, and is running, you should see a message similar to the following being output upon running the above command: + +`System clock synchronized: yes` + +#### Install an NTP Client + +If you do not see the message, you can install a NTP client using the following command: + +`sudo apt-get install ntp` + +Once installed, `ntpd` (the NTP client daemon) will start automatically. You can then query to ensure that system clock has synchronized by repeating the steps under [Checking System Clock Synchronization](/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md#checking-system-clock-synchronization). + +:::danger +If you skip this step, your Validator may miss chances to author blocks. If the clock isn't accurate, the network may not accept the blocks you produce. This will result in ImOnline heartbeats making it on chain, but zero allocated blocks making it on chain. +::: + +### Running a Relaychain Node + +You can refer to the page on [Running a Relaychain Node](/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/02-run-relaychain-node.md) in order to get setup. + +#### Synchronize Chain State + +In order to validate, you must first have a node that has fully synchronized. This is simply a matter of running your node and allowing it sufficient time to complete its synchronization. It may take several hours to complete this process. + +The syncing process can be identified as a result of lines been written to your console indicating "Syncing," like so: + +```text +2023-10-10 19:39:00 ⚙️ Syncing, target=#1861415 (8 peers), best: #1733 (0xf5eb…68ab), finalized #1536 (0x14c5…d421), ⬇ 1.0MiB/s ⬆ 15.5kiB/s +2023-10-10 19:39:05 ⚙️ Syncing 439.6 bps, target=#1861415 (8 peers), best: #3931 (0xa854…ea3c), finalized #3584 (0xc223…cab1), ⬇ 580.4kiB/s ⬆ 2.7kiB/s +2023-10-10 19:39:10 ⚙️ Syncing 428.6 bps, target=#1861416 (8 peers), best: #6074 (0x7a75…e2db), finalized #5632 (0x4944…9413), ⬇ 579.2kiB/s ⬆ 1.7kiB/s +2023-10-10 19:39:15 ⚙️ Syncing 417.2 bps, target=#1861417 (8 peers), best: #8160 (0xbb52…b682), finalized #7680 (0x61ce…91bc), ⬇ 577.6kiB/s ⬆ 1.4kiB/s +2023-10-10 19:39:20 ⚙️ Syncing 416.2 bps, target=#1861418 (8 peers), best: #10241 (0x3303…f6a6), finalized #10240 (0xaac3…9f1c), ⬇ 582.3kiB/s ⬆ 4.0kiB/s +``` + +You will know when it has synced, as you will no longer see lines as illustrated above and instead you'll see lines indicating blocks have been "Imported": + +```text +2023-10-18 14:33:24 ✨ Imported #1971895 (0xa37a…4ec9) +2023-10-18 14:33:24 ✨ Imported #1971895 (0xa37a…4ec9) +2023-10-18 14:33:22 💤 Idle (10 peers), best: #1971894 (0xe81a…d878), finalized #1971891 (0xa9fc…7deb), ⬇ 4.9kiB/s ⬆ 9.1kiB/s +2023-10-18 14:33:19 💤 Idle (9 peers), best: #1971894 (0xe81a…d878), finalized #1971890 (0x7065…145e), ⬇ 5.2kiB/s ⬆ 3.9kiB/s +2023-10-18 14:33:18 ✨ Imported #1971894 (0xe81a…d878) +2023-10-18 14:33:18 ✨ Imported #1971894 (0xe81a…d878) +2023-10-18 14:33:17 💤 Idle (10 peers), best: #1971893 (0xb933…472d), finalized #1971890 (0x7065…145e), ⬇ 7.1kiB/s ⬆ 6.1kiB/s +2023-10-18 14:33:14 💤 Idle (9 peers), best: #1971893 (0xb933…472d), finalized #1971890 (0x7065…145e), ⬇ 8.6kiB/s ⬆ 10.7kiB/s +``` + +#### Session Keys + +If you're uncertain about your node's current session keys after the setKeys transaction, you can check using two RPC methods: hasKey for a specific key or hasSessionKeys for the entire session key string. + +Once your node has been fully synced, it's now time to generate your session keys. You need to stop the node and run it again using the `--validator` option. Running with the `--validator` option also requires you to specify the `--public-addr` option. You can do this by first acquiring your public IP address (ex. `198.51.100.69`) and then passing: `--public-addr /ip4/198.51.100.69/tcp/30334/ws` + +`$ ./enjin --validator --rpc-methods=unsafe --name "a_reasonably_unique_name_a8f310" --public-addr /ip4/[YOUR-IP]/tcp/30334/ws` + +You will know if this was successful as you will see an output during startup indicating your role is now `AUTHORITY` and an indication of the `BABE` authorship worker being started. This can be observed in the below example: + +```text +2025-08-15 08:34:07 Enjin +2025-08-15 08:34:07 ✌️ version 1.6.0-2dafb82dcb4 +2025-08-15 08:34:07 ❤️ by Enjin, 2017-2025 +2025-08-15 08:34:07 📋 Chain specification: Enjin Relaychain +2025-08-15 08:34:07 🏷 Node name: a_reasonably_unique_name_a8f310 +2025-08-15 08:34:07 👤 Role: AUTHORITY +2025-08-15 08:34:07 💾 Database: RocksDb at /enjin/chainstate/relaychain/chains/enjin-relaychain/db/full +[..] +2025-08-15 08:34:09 👶 Starting BABE Authorship worker +``` + +#### Generating Session Keys + +:::tip +If you do not have curl installed, you can install it using the following command: `sudo apt-get install curl` +::: + +You need to tell the chain about your Session Keys. This is what associates your validator node with your stash account. You can do this by signing and submitting an extrinsic. + +Before you can do this, you must first make add an additional option to allow for unsafe RPC methods to be performed: `--rpc-methods=unsafe` + +If you are using the binary, simply add this option to the end of your command. If you are using Docker, you will need to edit the `docker-compose.yml` file and the `command` field therein to include this. + +You must first call the `author_rotateKeys` on your node. You can do this via CLI using the following `curl` command: + +``` +curl -X POST \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"author_rotateKeys","params":[],"id":1}' \ + http://localhost:9944 +``` + +:::info +If you have changed the default ports for your node, you will need to modify the curl command to reflect which port exposes the RPC interface. +::: + +If you ran this successfully, you will receive a successful response and the result will include a hex-encoded string. This string is your Session Keys. It is important you make note of these keys as we will set it in the next session. + +`{"jsonrpc":"2.0","result":"0xef1b41a7fca69a26633deb436f3b11aa94eab64edbb9b41e290a1487bb9c9744[..]3893e7a76d3c7ec070a30c96ed1fd2b22daa6856c79ce709032891d4afd171c9","id":1}` + +**Make sure to remove the options exposing the RPC interface (`--rpc-external`, `--rpc-cors=all`, `--rpc-methods=unsafe`) that was added earlier as they are incompatible when running a node with `--validator`.** + +#### Bond ENJ, Set Session Keys and Begin Validating + +To become a Validator on the Enjin Relaychain, you initially need 15,000 ENJ. However, to become an Active Validator and earn rewards, you need a total stake that meets or exceeds a certain amount of ENJ. + +If you want community nominations, you must show commitment and trust by bonding some of your own ENJ and illustrating a willingness of own personal loss if you're to operate the Validator incorrectly. Don't bond all of your ENJ to ensure you can cover transaction fees. Bonded ENJ is locked and cannot be used. + +In this step, we will bond an amount ENJ, set our session keys and confirm we're ready to begin validating. To get started, navigate to the [Staking \> Accounts](https://console.enjin.io/?rpc=wss%3A%2F%2Frpc.relay.blockchain.enjin.io#/staking/actions) page on the Enjin Console and select `+ Add Validator`. + +![](/img/components/blockchain-infrastructure/2.png) + + +Select the `stash account` that you want to use as a Validator. This account will be responsible for bonding the relevant ENJ. + +Enter the amount that you would like to bond into the `value bonded` field. In this example we're using 15,000 ENJ. Remember that, if you want to be considered an Active Validator you must bond sufficient ENJ to exceed the current minimum active stake. + +For the `payment destination` there are three options: + +- **Stash account (increase the amount at stake):** this option allows your Validator's stake to keep increasing with each payout. This can be beneficial as it results in your Validator's stake (and, in-turn, your total stake) to keep increasing and thereby make you more likely to remain an Active Validator by being in the top 13. +- **Stash account (do not increase the amount at stake):** this option allows you to receive ENJ directly back to your stash account, though the amount is not added to your Validator's stake. +- **Specified payment account:** this option allows you to specify an alternative account (unrelated to the stash account) which will receive any payouts. +- You can then select `next` to continue the setup. + +![](/img/components/blockchain-infrastructure/3.png) + +In the `keys from rotatekeys` field, you want to enter the keys you acquired during Generating Session Keys. + +For the `reward commission percentage`, this is a value ranging from 1 - 100. This is the rate that your Validator will be commissioned with. + +Selecting a high commission percentage means you are less likely to be nominated by a nominator. Especially, during the early stages of you operating a Validator where proving your reputation is paramount. + +In the `allow new nominations` field. This controls whether you want to allow nomination pools to nominate your Validator. Selecting `Yes, allow nominations` will allow your Validator to be nominated by nominators. Selecting `No, block all nominations` will prevent your Validator from being nominated by any new nomination pools. + +Finally, select `Bond & Validate` to finalise the setup of your Validator. + +All that remains is to verify everything was set correctly. You can do this by navigating to the [Staking \> Overview](https://console.enjin.io/?rpc=wss%3A%2F%2Frpc.relay.blockchain.enjin.io#/staking) section of the Enjin Console and selecting the `Waiting` tab. If everything was successful, you'll see your Validator listed. + +![](/img/components/blockchain-infrastructure/4.png) + +Congratulations! If you've followed everything correctly, you're now officially a Validator! Make sure that you have enough total stake backing your Validator be selected for the next Validator Set. + +## FAQ + +### Validator Set - when are new Validators selected? + +Validators are selected at the start of the final epoch in a given era. For the Enjin Relaychain, this means on the 6th epoch (20th hour) of the era. In order to be selected, you must be one of the top 13 Validators based on total stake and not currently chilled or slashed. + +### When will my Validator become active? + +If you were selected to be part of the next Validator Set, your Validator will become active in the next era. For the Enjin Relaychain, this means 4 hours after your Validator was selected for inclusion within the next Validator Set. diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/02-security-best-practices-for-validators.md b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/02-security-best-practices-for-validators.md new file mode 100644 index 0000000..e4e5aea --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/02-security-best-practices-for-validators.md @@ -0,0 +1,48 @@ +--- +title: "Security Best Practices for Validators" +slug: "security-best-practices" +description: "Validators play a critical role in the Enjin Blockchain ecosystem by proposing, validating and finalizing blocks. Given their importance, validators are frequent targets for various types of attacks. It is essential to follow best practices for setting up and maintaining your validator nodes to mitigate risk and ensure the network’s robustness." +--- +## Secure Server Configuration + +The security of the servers where validator nodes are run is fundamental to maintaining a secure validator. Here are the key considerations for securing your server: + +- **Use of Verified Software:** Always download Docker images from the official Docker Hub repository ([enjin/relaychain](https://hub.docker.com/r/enjin/relaychain)). This helps ensure that you are using the correct, unmodified, software necessary for network participation. +- **Up-to-Date Software:** Ensuring you’re running the latest stable release is important for both operating the node (you can sign up to our mailing list to keep up-to-date: [mailing-list-node-operators@enjin.io](https://groups.google.com/a/enjin.io/g/mailing-list-node-operators)), but also keeping your operating system and other applications up-to-date is equally essential to patch 0-days and prevent other such potential vulnerabilities. +- Verify File Integrity: After downloading files, verify the hashes against those provided by the download source. This step ensures that the files have not been tampered with or corrupted during the download process. +- **Follow Official Validator Setup Documentation:** Use the [Running a Validator](/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md) documentation to set up a validator node and ensure adhering to its principles. +- **Dedicated Server for Validator Nodes:** A validator node is an incredibly sensitive operation and running one requires careful considerations. One of those is ensuring the server running the validator node is dedicated for the sole purpose of running a single validator node. This is to ensure compute resources are dedicated to the node and to prevent other applications from interfering with the execution of the node. +- **Do Not Run as Privileged User:** Never use the root or admin user. Validators should run as a non-privileged user to minimize potential damage from attacks. + +### Linux-specific Best Practices + +It is strongly recommended that you disable non-essential SSH subsystems like banner, motd, scp, and X11 forwarding. Furthermore, preventing the use of password-based SSH in favor of key-based access will strongly improve your node’s security. + +## Network Security + +A well-configured network is crucial to protecting your validator from external threats. Below are network security best practices to follow: + +- **Firewalls and Port Management:** Protect your node from unauthorized access, you can read more on the [official documentation](/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-running-a-node.md#ports) about which ports need to be opened and the rest can remain restricted. +- **Secure Access to Validator:** Ensure access to your validator is as restricted as possible. Use a Virtual Private Network (VPN) or SSH tunnel for secure remote access. Use IP whitelisting to allow only specific IP addresses to connect to your validator. When possible, make sure to enforce Multi-Factor Authentication (MFA) for any account with access to the validator. + +## Key Management and Secure Access + +Your validator node’s private key is the most sensitive asset. Proper key management and secure access protocol are essential to preventing key theft or misuse: + +- **Secure Private Key Storage and Generation:** Store your validator key on a Hardware Security Module (HSM) or a Hardware (Cold) Wallet to ensure it is highly secure and not exposed to the host operating system. +- **Validator Access Control:** Apply the principle of least privilege to all accounts and services. Ensure that users and processes have only the minimal level of access necessary to perform their functions. This reduces the risk of privilege escalation attacks or accidental key exposure to those that don't require access. +- **Regular Auditing:** reviewing access logs, security settings and applying ongoing updates based on changes in best practices. + +## Monitoring and Incident Response + +Continuous monitoring and an effective incident response are key to maintaining a secure validator setup. The following are best practices for setting up monitoring and incident response procedures: + +- **Real-Time Monitoring:** Implement real-time monitoring of your validator node, including performance, used resources (CPU, memory, disk), network traffic and validator health. Use tools like Prometheus and Grafana, and make sure to set up alerts for abnormal activities. +- **Logging and Auditing:** Implement comprehensive logging for your validator node. Store and maintain these logs securely. In the event of an issue or security incident, these logs can be audited to identify the root cause and facilitate a timely resolution. +- **Incident Response:** Create a team responsible for quick response to any incidents. The team should be able to answer incidents 24/7. +- **Backup and Recovery:** Implement a backup strategy that includes regular snapshots of the chainstate. Test recovery procedures periodically to ensure they are up-to-date and effective. +- **Validator Supervision:** Configure your validator software to be restarted automatically if stopped for any reason. Use a supervisor process to manage this. Review the logs associated with the commanded shutdown. + +## Conclusion + +Following these security best practices will help ensure your validator node is secure and compliant with the standards of the Enjin Blockchain. The security of validators is vital to maintaining the trust and functionality of the network. diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/03-how-to-stop-validating.md b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/03-how-to-stop-validating.md new file mode 100644 index 0000000..dee0288 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/03-how-to-stop-validating.md @@ -0,0 +1,66 @@ +--- +title: "How to Stop Validating" +slug: "stop-validating" +description: "If you are currently operating a validator node on the Enjin Blockchain and wish to stop validating, follow the steps outlined below." +--- +Stopping validation may be necessary for various reasons, including switching roles, maintaining the node, upgrading the node, or withdrawing from the network as a validator. This guide will walk you through the process to ensure your transition out of the active set of validators is smooth and efficient. If you only wish to stop validating temporarily for maintenance, Step 1 is all you need to perform. If you wish to unbond funds and reap an account, Steps 1-3 should be followed. + +## Step 1: Chill Your Validator + +“Chilling” your validator means that it will no longer be an active participant in the validator set, effectively stopping it from validating blocks and earning rewards. Chilling does not affect your staked tokens immediately, it simply signals to the network that you wish to stop validation. It’s a few simple steps to chill your validator. + +:::danger Important +If your validator node is part of the active validator set, it is imperative that you **DO NOT** immediately turn off your validator. Chilling is a signal of intent and will ensure you’re not selected upon the next session selection. Wait until your validator node is no longer part of an active validator set before turning it offline. + +You may need to wait up to the conclusion of 7 epochs (28 hours) to be removed from being an active validator. +::: + +### Enjin Console + +- Open the [Enjin Console](https://console.enjin.io/#/staking/actions) with your validator account. +- Navigate to Network \> Staking \> Account. +- Click “Stop” next to your validator. + +### Programmatically + +- Connect to the Enjin RPC node with your validator account. +- Send the `staking.chill()` extrinsic. + +Once your validator is chilled, it will no longer be eligible for selection in future staking sessions and it will stop producing blocks. + +## Step 2: Purge Validator Session Keys + +:::danger Important +Purging session keys is a permanent action. Once completed, your validator will no longer be able to participate in validation activities unless new keys are generated and [set up again](/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md). + +This step is crucial for security, as it prevents any accidental or unauthorized activation of your validator. +::: + +After chilling your validator, the next crucial step is to purge the session keys associated with your validator. Purging the session keys removes association of your validator node with your stash account. + +- Open [Enjin Console](https://console.enjin.io/#/extrinsics). +- Navigate to Developer \> Extrinsics. +- Select the Correct Account. It should be the same account that was used to set the keys in the first place.Call the `session.purge_keys()` extrinsic. + +## Step 3: Unbond Your Tokens + +Unbonding will release your ENJ staked as a validator. Once unbonded, your tokens will become freely transferable. You can follow the below steps to unbond your staked ENJ. + +### Enjin Console + +- Open the Enjin Console with your stash account. +- Navigate to Network \> Staking \> Account actions. +- Click the corresponding stash account dropdown and select “Unbond funds”. + +### Programmatically + +- Connect to the Enjin RPC node with your stash account. +- Send `staking.unbond()` extrinsic. + +## Conclusion + +To temporarily stop validating on the Enjin Blockchain you have only to chill your validator. + +To permanently stop validating, you must follow three primary steps: chilling your validator, purging the validator’s session keys, and unbonding your tokens. + +By following these steps carefully, you can ensure a secure and smooth transition out of your role as a validator without any accidental penalties or disruptions. diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/_category_.json b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/_category_.json new file mode 100644 index 0000000..d9d1e76 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Operating a Relaychain Validator", + "link": { + "type": "generated-index", + "description": "This section takes you on the journey of operating a Relaychain Validator Node." + } +} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/_category_.json b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/_category_.json new file mode 100644 index 0000000..68ca060 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Running Nodes", + "link": { + "type": "generated-index", + "description": "Learn how to set up and operate various types of nodes and validators on the Enjin Blockchain, including relay chain and matrix chain nodes." + } +} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/03-running-enjin-indexer.md b/versioned_docs/version-2.x/02-guides/02-blockchain/03-running-enjin-indexer.md new file mode 100644 index 0000000..319c11a --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/02-blockchain/03-running-enjin-indexer.md @@ -0,0 +1,220 @@ +--- +title: "Running the Enjin Indexer" +slug: "running-enjin-indexer" +description: "The Enjin Indexer is a tool that processes and serves blockchain data for applications that require efficient data retrieval, filtering, and relations. This guide will walk you through setting up and running the indexer step-by-step." +--- +## Prerequisites + +Before you begin, ensure that you have [installed Docker](https://docs.docker.com/get-started/get-docker/) on a machine you will use to host the Indexer. + +## Quick Start + +1. Begin by creating three files: (`docker-compose.yml`, `.env` and `init.sql`): + +```yaml +# docker-composer.yml + +version: "3.9" + +services: + indexer_db: + container_name: indexer_db + image: postgres:16 + restart: unless-stopped + environment: + POSTGRES_DB: ${DB_NAME} + POSTGRES_PASSWORD: ${DB_PASS} + volumes: + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + - indexer_db:/var/lib/postgresql/data + ports: + - "5432:5432" + command: [ + "postgres", + "-c", "max_connections=200" + ] + + indexer_redis: + container_name: indexer_redis + image: redis:7.0 + restart: on-failure + volumes: + - redis_db:/data + ports: + - "6389:6379" + + indexer_processor: + container_name: indexer_processor + restart: unless-stopped + image: enjin/indexer:${VERSION:-latest} + environment: + CONTAINER_ROLE: processor + build: + context: . + volumes: + - .env:/squid/.env + depends_on: + - indexer_db + - indexer_worker + + indexer_graphql: + container_name: indexer_graphql + restart: unless-stopped + image: enjin/indexer:${VERSION:-latest} + environment: + CONTAINER_ROLE: graphql + build: + context: . + ports: + - "4000:4000" + - "8080:8080" + volumes: + - .env:/squid/.env + depends_on: + - indexer_db + - indexer_processor + + indexer_worker: + container_name: indexer_worker + restart: unless-stopped + image: enjin/indexer:${VERSION:-latest} + environment: + CONTAINER_ROLE: worker + build: + context: . + ports: + - "9090:9090" + volumes: + - .env:/squid/.env + depends_on: + - indexer_db + - indexer_redis + +volumes: + indexer_db: + redis_db: +``` + +```sql +-- init.sql + +CREATE SCHEMA IF NOT EXISTS metadata; + +CREATE TABLE IF NOT EXISTS "metadata"."metadata" ( + id TEXT PRIMARY KEY, + metadata JSONB, + uri TEXT, + last_updated_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS metadata_uri ON "metadata"."metadata" (uri); +``` + +### .dotenv Configurations + +#### Enjin Relaychain +```dotenv +# .env (for Enjin Relaychain) + +ARCHIVE_ENDPOINT=https://v2.archive.subsquid.io/network/enjin-relay +CHAIN_ENDPOINT=wss://archive.relay.blockchain.enjin.io +CHAIN_PREFIX=2135 +DB_HOST=indexer_db +DB_NAME=indexer_enjin_relay +DB_PORT=5432 +DB_PASS=postgres +REDIS_URL=redis://indexer_redis:6379/0 +``` + +#### Enjin Matrixchain +```dotenv +# .env (for Enjin Matrixchain) + +ARCHIVE_ENDPOINT=https://v2.archive.subsquid.io/network/enjin-matrix +CHAIN_ENDPOINT=wss://archive.matrix.blockchain.enjin.io +CHAIN_PREFIX=1110 +DB_HOST=indexer_db +DB_NAME=indexer_enjin_matrix +DB_PORT=5432 +DB_PASS=postgres +REDIS_URL=redis://indexer_redis:6379/1 +``` + +#### Canary Relaychain +```dotenv +# .env (for Canary Relaychain) + +ARCHIVE_ENDPOINT=# no archive endpoint available for Canary Relaychain +CHAIN_ENDPOINT=wss://archive.relay.canary.enjin.io +CHAIN_PREFIX=69 +DB_HOST=indexer_db +DB_NAME=indexer_canary_relay +DB_PORT=5432 +DB_PASS=postgres +REDIS_URL=redis://indexer_redis:6379/2 +``` + +#### Canary Matrixchain +```dotenv +# .env (for Canary Matrixchain) + +ARCHIVE_ENDPOINT=https://v2.archive.subsquid.io/network/enjin-canary-matrix +CHAIN_ENDPOINT=wss://archive.matrix.canary.enjin.io +CHAIN_PREFIX=9030 +DB_HOST=indexer_db +DB_NAME=indexer_canary_matrix +DB_PORT=5432 +DB_PASS=postgres +REDIS_URL=redis://indexer_redis:6379/3 +``` +
+ +2. Start the Indexer by running the following command: `docker compose up -d` + _The command will launch all the necessary services in the background._ +3. Access the GraphQL API + Once the services are up and running, you can access the GraphQL Playground of the Indexer by opening your web browser and navigating to: http://localhost:4000/graphql + +You can read the [GraphQL Documentation](https://graphql.org/learn/) for more information about how to construct queries for the GraphQL interface. + +### Advanced Configuration + +1. Clone and Enter the Repository + ```bash + git clone https://github.com/enjin/indexer.git + cd indexer + ``` +2. Configure Environment Variables + In the repository, you will find a `.env.example` file that outlines the necessary environment variables for running the Enjin Indexer. Copy this file to create your own `.env` file: `cp .env.example .env` + Then, open the .env file in a text editor and fill in the missing values. Below is an example of the environment variables and their purpose: + +```text +NODE_ENV: Node.js environment (supported values: development, production) + +CHAIN_ENDPOINT: [Required] WebSocket endpoint to connect to. It is recommended to be set to your own Enjin archive node. +CHAIN_PREFIX: [Required] The chain prefix (SS58) of the chain you are connecting to. +ARCHIVE_ENDPOINT: [Optional] Subsquid archive endpoint for historical blockchain data. See: https://docs.sqd.ai/subsquid-network/reference/networks/https://docs.sqd.ai/subsquid-network/reference/networks/ + +DB_NAME: [Required] Postgres database name. +DB_HOST: [Required] Postgres host. +DB_PORT: [Required] Postgres port. +DB_PASS: [Required] Postgres password. +REDIS_URL: [Required] Redis URL. + +PROMETHEUS_PORT: [Optional] Prometheus port for metrics. +PROCESSOR_PROMETHEUS_ENDPOINT: [Optional] Prometheus endpoint for the processor. + +SQD_DEBUG: [Optional] Debugging flag for Subsquid. More info can be found at: https://docs.sqd.dev/sdk/reference/logger/ +SENTRY_DSN: [Optional] Sentry DSN for error tracking. +``` + +:::info +If you are using your own Redis or PostgresSQL instances (instead of the ones provided by Docker Compose), remember to remove the `indexer_db` and `indexer_redis` services from the `docker-compose.yml`, and set the database connection details (like `DB_HOST`, `DB_PORT` and `REDIS_URL`) in the `.env` file to match your external services. +::: + +## Troubleshooting + +- Ensure Docker is installed and running with sufficient resources. +- Use docker `compose logs -f ` to view logs and diagnose issues. +- Double-check all environment variables and configurations in the `.env` file. + +For additional support, you can reach out to [Enjin Support](https://support.enjin.io). diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/_category_.json b/versioned_docs/version-2.x/02-guides/02-blockchain/_category_.json new file mode 100644 index 0000000..d034661 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/02-blockchain/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Blockchain", + "link": { + "type": "generated-index", + "description": "Explore guides for running and operating components of the Enjin Blockchain. Learn how to set up nodes, operate validators, utilize the Matrixchain Indexer, and engage with staking and nomination pools on the Relaychain." + } +} + \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/_category_.json b/versioned_docs/version-2.x/02-guides/_category_.json new file mode 100644 index 0000000..7952236 --- /dev/null +++ b/versioned_docs/version-2.x/02-guides/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Integration Guides", + "link": { + "type": "generated-index", + "description": "Integrate with Enjin's platform and blockchain technologies. Find guides on managing tokens, integrating users, running nodes, and using the SDK." + } +} \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/01-queries/01-transactions-queries.md b/versioned_docs/version-2.x/03-api-reference/01-queries/01-transactions-queries.md new file mode 100644 index 0000000..1d274ad --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/01-queries/01-transactions-queries.md @@ -0,0 +1,1116 @@ +--- +title: "Transactions" +slug: "transactions" +description: "Use the Enjin API to query transaction data, including blockchain transactions history, status, and details." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip Core Endpoints +- **Testnet:** `http://platform.canary.enjin.io/graphql` +- **Mainnet:** `http://platform.enjin.io/graphql` +::: + +This is an overview of some of the most commonly used operations in the Enjin Platform Schema. + +## GetBlocks + +The `GetBlocks` query allows you to retrieve an array of blocks(), which can be filtered based on specific block numbers, transaction IDs, or hashes. It is designed to fetch detailed data on a set of blocks, providing valuable insights. + + + +```graphql +query GetBlocks { + GetBlocks(numbers: ["402865"]) { + totalCount + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + } + edges { + cursor + node { + id + number + hash + synced + failed + exception + } + } + } +} +``` + + +```json +{ + "data": { + "GetBlocks": { + "totalCount": 1, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasPreviousPage": false, + "hasNextPage": false + }, + "edges": [ + { + "cursor": "eyJudW1iZXIiOjQwMjg2NSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "id": 51507, + "number": 402865, + "hash": "0xf0b3cee1c36a99e24aaef7da06aec2ecd79a599c19ad4ae6fb4b40b3e497a322", + "synced": true, + "failed": false, + "exception": null + } + } + ] + } + } +} +``` + + + +## GetTransaction + +The `GetTransaction` query allows you to retrieve detailed information about a specific transaction. It provides comprehensive details about the transaction, including its state, associated wallet, and related events. + + + +```graphql +query GetTransaction { + GetTransaction( + id: 11300 + ) { + id + idempotencyKey + transactionId + transactionHash + method + state + result + encodedData + signedAtBlock + createdAt + updatedAt + wallet { + account { + publicKey + address + } + } + events { + edges { + cursor + node { + phase + lookUp + moduleId + eventId + params { + type + value + } + } + } + totalCount + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } +} +``` + + +```json +{ + "data": { + "GetTransaction": { + "id": 11300, + "idempotencyKey": "61f0b8e1-e0b9-4ecd-b445-84dc4cd2b495", + "transactionId": "402811-2", + "transactionHash": "0xbafe459e8248b802f3aef98d2e4a695bbb238899edf40519b082366e3ff8b98f", + "method": "BatchMint", + "state": "FINALIZED", + "result": "EXTRINSIC_SUCCESS", + "encodedData": "0x280dc56f04985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d05000180400010000c16ff2862300000000000000000001000000000000", + "signedAtBlock": 402809, + "createdAt": "2023-06-23T05:20:52+00:00", + "updatedAt": "2023-06-23T05:21:26+00:00", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "events": { + "edges": [ + { + "cursor": "eyJpZCI6Njk3MTEsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "phase": 2, + "lookUp": "unknown", + "moduleId": "Balances", + "eventId": "Withdraw", + "params": [ + { + "type": "who", + "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" + }, + { + "type": "amount", + "value": "19981763653182275" + } + ] + } + }, + { + "cursor": "eyJpZCI6Njk3MTIsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "phase": 2, + "lookUp": "unknown", + "moduleId": "Balances", + "eventId": "Reserved", + "params": [ + { + "type": "who", + "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" + }, + { + "type": "amount", + "value": "10000000000000000" + } + ] + } + }, + { + "cursor": "eyJpZCI6Njk3MTMsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "phase": 2, + "lookUp": "unknown", + "moduleId": "MultiTokens", + "eventId": "TokenCreated", + "params": [ + { + "type": "collection_id", + "value": "7153" + }, + { + "type": "token_id", + "value": "6" + }, + { + "type": "issuer", + "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" + }, + { + "type": "initial_supply", + "value": "1" + } + ] + } + }, + { + "cursor": "eyJpZCI6Njk3MTQsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "phase": 2, + "lookUp": "unknown", + "moduleId": "MultiTokens", + "eventId": "TokenAccountCreated", + "params": [ + { + "type": "collection_id", + "value": "7153" + }, + { + "type": "token_id", + "value": "6" + }, + { + "type": "account", + "value": "985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050" + }, + { + "type": "balance", + "value": "1" + } + ] + } + }, + { + "cursor": "eyJpZCI6Njk3MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "phase": 2, + "lookUp": "unknown", + "moduleId": "MultiTokens", + "eventId": "Minted", + "params": [ + { + "type": "collection_id", + "value": "7153" + }, + { + "type": "token_id", + "value": "6" + }, + { + "type": "issuer", + "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" + }, + { + "type": "recipient", + "value": "985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050" + }, + { + "type": "amount", + "value": "1" + } + ] + } + }, + { + "cursor": "eyJpZCI6Njk3MTYsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "phase": 2, + "lookUp": "unknown", + "moduleId": "Balances", + "eventId": "Deposit", + "params": [ + { + "type": "who", + "value": "6d6f646c65662f66656469730000000000000000000000000000000000000000" + }, + { + "type": "amount", + "value": "19981763653182275" + } + ] + } + }, + { + "cursor": "eyJpZCI6Njk3MTcsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "phase": 2, + "lookUp": "unknown", + "moduleId": "TransactionPayment", + "eventId": "TransactionFeePaid", + "params": [ + { + "type": "who", + "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" + }, + { + "type": "actual_fee", + "value": "19981763653182275" + }, + { + "type": "tip", + "value": "0" + } + ] + } + }, + { + "cursor": "eyJpZCI6Njk3MTgsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "phase": 2, + "lookUp": "unknown", + "moduleId": "System", + "eventId": "ExtrinsicSuccess", + "params": [ + { + "type": "dispatch_info", + "value": "{\"weight\":{\"ref_time\":\"848559950\",\"proof_size\":\"10283\"},\"class\":\"Normal\",\"pays_fee\":\"Yes\"}" + } + ] + } + } + ], + "totalCount": 8, + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": "", + "endCursor": "" + } + } + } + } +} +``` + + + +## GetTransactions + +The `GetTransactions` query in allows you to retrieve an array of transactions. You can optionally filter these transactions based on various parameters such as transaction IDs, transaction hashes, methods, states, results, or accounts. + + + +```graphql +query GetTransactions { + GetTransactions( + ids: [11300] + ) { + edges { + cursor + node { + id + idempotencyKey + transactionId + transactionHash + method + state + result + encodedData + signedAtBlock + createdAt + updatedAt + wallet { + account { + publicKey + address + } + } + events { + edges { + cursor + node { + phase + lookUp + moduleId + eventId + params { + type + value + } + } + } + totalCount + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + } + totalCount + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + } + } +} +``` + + +```json +{ + "data": { + "GetTransactions": { + "edges": [ + { + "cursor": "eyJpZCI6MTEzMDAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "id": 11300, + "idempotencyKey": "61f0b8e1-e0b9-4ecd-b445-84dc4cd2b495", + "transactionId": "402811-2", + "transactionHash": "0xbafe459e8248b802f3aef98d2e4a695bbb238899edf40519b082366e3ff8b98f", + "method": "BatchMint", + "state": "FINALIZED", + "result": "EXTRINSIC_SUCCESS", + "encodedData": "0x280dc56f04985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d05000180400010000c16ff2862300000000000000000001000000000000", + "signedAtBlock": 402809, + "createdAt": "2023-06-23T05:20:52+00:00", + "updatedAt": "2023-06-23T05:21:26+00:00", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "events": { + "edges": [ + { + "cursor": "eyJpZCI6Njk3MTEsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "phase": 2, + "lookUp": "unknown", + "moduleId": "Balances", + "eventId": "Withdraw", + "params": [ + { + "type": "who", + "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" + }, + { + "type": "amount", + "value": "19981763653182275" + } + ] + } + }, + { + "cursor": "eyJpZCI6Njk3MTIsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "phase": 2, + "lookUp": "unknown", + "moduleId": "Balances", + "eventId": "Reserved", + "params": [ + { + "type": "who", + "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" + }, + { + "type": "amount", + "value": "10000000000000000" + } + ] + } + }, + { + "cursor": "eyJpZCI6Njk3MTMsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "phase": 2, + "lookUp": "unknown", + "moduleId": "MultiTokens", + "eventId": "TokenCreated", + "params": [ + { + "type": "collection_id", + "value": "7153" + }, + { + "type": "token_id", + "value": "6" + }, + { + "type": "issuer", + "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" + }, + { + "type": "initial_supply", + "value": "1" + } + ] + } + }, + { + "cursor": "eyJpZCI6Njk3MTQsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "phase": 2, + "lookUp": "unknown", + "moduleId": "MultiTokens", + "eventId": "TokenAccountCreated", + "params": [ + { + "type": "collection_id", + "value": "7153" + }, + { + "type": "token_id", + "value": "6" + }, + { + "type": "account", + "value": "985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050" + }, + { + "type": "balance", + "value": "1" + } + ] + } + }, + { + "cursor": "eyJpZCI6Njk3MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "phase": 2, + "lookUp": "unknown", + "moduleId": "MultiTokens", + "eventId": "Minted", + "params": [ + { + "type": "collection_id", + "value": "7153" + }, + { + "type": "token_id", + "value": "6" + }, + { + "type": "issuer", + "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" + }, + { + "type": "recipient", + "value": "985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050" + }, + { + "type": "amount", + "value": "1" + } + ] + } + }, + { + "cursor": "eyJpZCI6Njk3MTYsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "phase": 2, + "lookUp": "unknown", + "moduleId": "Balances", + "eventId": "Deposit", + "params": [ + { + "type": "who", + "value": "6d6f646c65662f66656469730000000000000000000000000000000000000000" + }, + { + "type": "amount", + "value": "19981763653182275" + } + ] + } + }, + { + "cursor": "eyJpZCI6Njk3MTcsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "phase": 2, + "lookUp": "unknown", + "moduleId": "TransactionPayment", + "eventId": "TransactionFeePaid", + "params": [ + { + "type": "who", + "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" + }, + { + "type": "actual_fee", + "value": "19981763653182275" + }, + { + "type": "tip", + "value": "0" + } + ] + } + }, + { + "cursor": "eyJpZCI6Njk3MTgsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "phase": 2, + "lookUp": "unknown", + "moduleId": "System", + "eventId": "ExtrinsicSuccess", + "params": [ + { + "type": "dispatch_info", + "value": "{\"weight\":{\"ref_time\":\"848559950\",\"proof_size\":\"10283\"},\"class\":\"Normal\",\"pays_fee\":\"Yes\"}" + } + ] + } + } + ], + "totalCount": 8, + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": "", + "endCursor": "" + } + } + } + } + ], + "totalCount": 1, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasPreviousPage": false, + "hasNextPage": false + } + } + } +} +``` + + + +## GetPendingEvents + +The `GetPendingEvents` query is designed to retrieve a list of events that have been broadcast by the system but not yet acknowledged by the client. This is useful for ensuring that no events are missed or unprocessed, particularly in systems that rely on event-driven architectures or asynchronous processing. + + + +```graphql +query GetPendingEvents{ + GetPendingEvents( + acknowledgeEvents: false + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + uuid + name + sent + channels + data + } + } + } +} +``` + + +```json +{ + "data": { + "GetPendingEvents": { + "totalCount": 95151, + "pageInfo": { + "startCursor": "", + "endCursor": "eyJpZCI6NjA3LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "hasNextPage": true, + "hasPreviousPage": false + }, + "edges": [ + { + "cursor": "eyJpZCI6NTkzLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "node": { + "id": 593, + "uuid": "b7678fd9-8447-453b-8821-172d6b84397b", + "name": "platform:beam-claims-complete", + "sent": "2023-05-28 15:08:07", + "channels": [ + "collection;101729", + "platform", + "beam;platform" + ], + "data": { + "id": 11, + "beam": { + "id": 2, + "end": "2028-11-14 00:00:00", + "code": "aa9c8deb3549e635ef2fc40a7285596a", + "name": "373", + "image": "https://en.wikipedia.org/wiki/Firefox#/media/File:Firefox_logo,_2019.svg", + "start": "2022-11-14 00:00:00", + "flags_mask": 0, + "description": "This is the most awesome Beam ever.", + "collection_chain_id": "101729" + }, + "type": "MINT_ON_DEMAND", + "state": "COMPLETED", + "quantity": 10, + "attributes": [], + "claimed_at": "2023-05-28 15:07:37", + "ip_address": "172.69.58.64", + "token_chain_id": "1283", + "wallet_public_key": "0x83800dc53431f36f0aaaad573eded3394c2c976d28d528111f82968f0a601bc2" + } + } + }, + { + "cursor": "eyJpZCI6NTk0LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "node": { + "id": 594, + "uuid": "50859a2b-574e-4d0d-8577-e8e53f9eee1d", + "name": "platform:transaction-created", + "sent": "2023-05-28 15:08:10", + "channels": [ + "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" + ], + "data": { + "id": 98, + "state": "PENDING", + "method": "CreateCollection", + "idempotencyKey": "575992c4-911a-4d59-8ccf-7958d46e605b" + } + } + }, + { + "cursor": "eyJpZCI6NTk1LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "node": { + "id": 595, + "uuid": "63c4c28d-463c-4455-bb9b-43ccbf89b53a", + "name": "platform:transaction-created", + "sent": "2023-05-28 15:08:10", + "channels": [ + "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" + ], + "data": { + "id": 99, + "state": "PENDING", + "method": "BatchSetAttribute", + "idempotencyKey": "0134b2af-b92d-4999-8900-3f8124172247" + } + } + }, + { + "cursor": "eyJpZCI6NTk2LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "node": { + "id": 596, + "uuid": "fe37740b-cb5b-4605-940a-d2b6927ca2d6", + "name": "platform:beam-claim-pending", + "sent": "2023-05-28 15:08:13", + "channels": [ + "collection;101729", + "platform", + "beam;platform" + ], + "data": { + "beam": { + "id": 2, + "end": "2028-11-14 00:00:00", + "code": "aa9c8deb3549e635ef2fc40a7285596a", + "name": "373", + "image": "https://en.wikipedia.org/wiki/Firefox#/media/File:Firefox_logo,_2019.svg", + "start": "2022-11-14 00:00:00", + "flags_mask": 0, + "description": "This is the most awesome Beam ever.", + "collection_chain_id": "101729" + }, + "state": "PENDING", + "extras": [], + "beam_id": 2, + "claimed_at": "2023-05-28T15:08:13.679083Z", + "ip_address": "172.70.126.55", + "idempotency_key": "0b193cff-8c97-4a72-881b-1adb4432da1d", + "wallet_public_key": "0x66f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f646009" + } + } + }, + { + "cursor": "eyJpZCI6NTk3LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "node": { + "id": 597, + "uuid": "b3923e2b-3f46-4166-951d-d074c05255a1", + "name": "platform:transaction-updated", + "sent": "2023-05-28 15:08:16", + "channels": [ + "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" + ], + "data": { + "id": 99, + "state": "PROCESSING", + "method": "BatchSetAttribute", + "result": null, + "transactionId": null, + "idempotencyKey": "0134b2af-b92d-4999-8900-3f8124172247", + "transactionHash": null + } + } + }, + { + "cursor": "eyJpZCI6NTk4LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "node": { + "id": 598, + "uuid": "9f4911be-3b0e-4604-8248-01db2f6c9255", + "name": "platform:transaction-updated", + "sent": "2023-05-28 15:08:16", + "channels": [ + "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" + ], + "data": { + "id": 98, + "state": "PROCESSING", + "method": "CreateCollection", + "result": null, + "transactionId": null, + "idempotencyKey": "575992c4-911a-4d59-8ccf-7958d46e605b", + "transactionHash": null + } + } + }, + { + "cursor": "eyJpZCI6NTk5LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "node": { + "id": 599, + "uuid": "b6cfd3bc-5850-415b-9db6-06f7ebea6f50", + "name": "platform:transaction-created", + "sent": "2023-05-28 15:08:16", + "channels": [ + "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" + ], + "data": { + "id": 100, + "state": "PENDING", + "method": "BatchMint", + "idempotencyKey": "b09251c5-7911-4d0c-ac02-89ab049aa713" + } + } + }, + { + "cursor": "eyJpZCI6NjAwLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "node": { + "id": 600, + "uuid": "2de8a2b2-10e7-4769-a432-53c6258e8721", + "name": "platform:beam-claim-in-progress", + "sent": "2023-05-28 15:08:16", + "channels": [ + "collection;101729", + "platform", + "beam;platform" + ], + "data": { + "id": 20, + "beam": { + "id": 2, + "end": "2028-11-14 00:00:00", + "code": "aa9c8deb3549e635ef2fc40a7285596a", + "name": "373", + "image": "https://en.wikipedia.org/wiki/Firefox#/media/File:Firefox_logo,_2019.svg", + "start": "2022-11-14 00:00:00", + "flags_mask": 0, + "description": "This is the most awesome Beam ever.", + "collection_chain_id": "101729" + }, + "quantity": 10, + "attributes": [], + "collection": { + "id": 98684, + "owner": { + "id": 2, + "address": "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", + "managed": 0, + "network": "developer", + "created_at": "2023-05-28T14:47:39.000000Z", + "public_key": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "updated_at": "2023-05-28T14:47:39.000000Z", + "external_id": null, + "verification_id": null + }, + "network": "developer", + "is_frozen": 0, + "created_at": "2023-05-28T15:06:30.000000Z", + "updated_at": "2023-05-28T15:06:30.000000Z", + "token_count": "0", + "total_deposit": "25000000000000000000", + "attribute_count": "0", + "max_token_count": "100", + "owner_wallet_id": 2, + "max_token_supply": "100", + "force_single_mint": 0, + "royalty_wallet_id": null, + "royalty_percentage": null, + "collection_chain_id": "101729" + }, + "token_chain_id": "1283", + "wallet_public_key": "0x66f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f646009" + } + } + }, + { + "cursor": "eyJpZCI6NjAxLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "node": { + "id": 601, + "uuid": "50d2f458-0565-41c7-94d3-c011f7528c9e", + "name": "platform:transaction-updated", + "sent": "2023-05-28 15:08:16", + "channels": [ + "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" + ], + "data": { + "id": 99, + "state": "BROADCAST", + "method": "BatchSetAttribute", + "result": null, + "transactionId": null, + "idempotencyKey": "0134b2af-b92d-4999-8900-3f8124172247", + "transactionHash": "0xd4e253623bc8170eef9cbe4537c08e063bd421ebb18b9943b9db7b75c5a0c03e" + } + } + }, + { + "cursor": "eyJpZCI6NjAyLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "node": { + "id": 602, + "uuid": "10b155ce-e514-4db8-9702-d1df475903a9", + "name": "platform:transaction-updated", + "sent": "2023-05-28 15:08:16", + "channels": [ + "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" + ], + "data": { + "id": 98, + "state": "BROADCAST", + "method": "CreateCollection", + "result": null, + "transactionId": null, + "idempotencyKey": "575992c4-911a-4d59-8ccf-7958d46e605b", + "transactionHash": "0xafecedea403e28722fb454b9be9bca5c55a9d0d7a62c30d80f2e2e64f78c4436" + } + } + }, + { + "cursor": "eyJpZCI6NjAzLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "node": { + "id": 603, + "uuid": "a6f22ab2-fd62-45a0-9c24-99b5c814f6b1", + "name": "platform:token-burned", + "sent": "2023-05-28 15:08:19", + "channels": [ + "collection;101728", + "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", + "token;70468841277235617716769448539773927607", + "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "platform" + ], + "data": { + "amount": "1", + "wallet": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "tokenId": "70468841277235617716769448539773927607", + "collectionId": "101728", + "idempotencyKey": "4259ee51-f38e-4e57-907d-3da671340917" + } + } + }, + { + "cursor": "eyJpZCI6NjA0LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "node": { + "id": 604, + "uuid": "c7703b9d-08cd-4350-b342-2c263975cde0", + "name": "platform:collection-created", + "sent": "2023-05-28 15:08:19", + "channels": [ + "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", + "platform" + ], + "data": { + "owner": "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", + "collectionId": "101732", + "idempotencyKey": "7c909267-1ff3-45c8-9651-b87a1697042a" + } + } + }, + { + "cursor": "eyJpZCI6NjA1LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "node": { + "id": 605, + "uuid": "2b8ac300-d970-4a56-8db5-e979d0827f47", + "name": "platform:collection-created", + "sent": "2023-05-28 15:08:19", + "channels": [ + "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", + "platform" + ], + "data": { + "owner": "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", + "collectionId": "101733", + "idempotencyKey": "492af562-c4e9-418d-9707-5ce4a24680e1" + } + } + }, + { + "cursor": "eyJpZCI6NjA2LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "node": { + "id": 606, + "uuid": "cfeca74d-b0cf-42d9-bb5a-b8bf553b3127", + "name": "platform:transaction-updated", + "sent": "2023-05-28 15:08:19", + "channels": [ + "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" + ], + "data": { + "id": 97, + "state": "FINALIZED", + "method": "Burn", + "result": "EXTRINSIC_SUCCESS", + "transactionId": "865545-2", + "idempotencyKey": "4259ee51-f38e-4e57-907d-3da671340917", + "transactionHash": "0xd6f47b7c9ef3479b788ecaf76a203ebbefdb96f74fce3cac75ad84b0fc49f191" + } + } + }, + { + "cursor": "eyJpZCI6NjA3LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "node": { + "id": 607, + "uuid": "98a3449f-ccba-4f73-9a33-d4d9d55f979f", + "name": "platform:transaction-updated", + "sent": "2023-05-28 15:08:19", + "channels": [ + "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" + ], + "data": { + "id": 95, + "state": "FINALIZED", + "method": "CreateCollection", + "result": "EXTRINSIC_SUCCESS", + "transactionId": "865545-3", + "idempotencyKey": "7c909267-1ff3-45c8-9651-b87a1697042a", + "transactionHash": "0x6b1cdfaafcc5b343c02ce4593d56e0ad3bf2ffc814baac5e699a43e73c147a21" + } + } + } + ] + } + } +} +``` + + + + +### Use Case: + +You can use the `GetPendingEvents` query to ensure that all events are accounted for and processed in your system, even in the presence of network issues or disruptions. This query is crucial for: + +- Reconciling transaction records. +- Updating user interfaces in real-time. +- Triggering downstream processes that depend on event data. + +Developers and system administrators can use this query to monitor and handle event-driven systems effectively, maintaining their reliability and consistency. To acknowledge the outputted events (and therefore remove them from pending events), set the `acknowledgeEvents` parameter to `true` (default is false). +To filter outputted pending events on specific channels, use the `channelFilters` parameter + +```graphql +query geFilteredtPendingEvents { + GetPendingEvents( + channelFilters:[ + {type:OR filter:"cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu"} + {type:OR filter:"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"} + ]){ + edges { + node { + id + uuid + data + channels + } + } + } +} +``` + +This example will output pending events broadcasted in either `cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu` and `cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f` channels. +Using the `Type` argument, you, can construct advanced filters like `'channel_1' AND ('channel_2' OR 'channel_3')` +Note, The default type is "**AND**". diff --git a/versioned_docs/version-2.x/03-api-reference/01-queries/02-collections-queries.md b/versioned_docs/version-2.x/03-api-reference/01-queries/02-collections-queries.md new file mode 100644 index 0000000..eb5c740 --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/01-queries/02-collections-queries.md @@ -0,0 +1,405 @@ +--- +title: "Collections" +slug: "collections" +description: "Query the Enjin API to retrieve data about collections, including metadata and token information within a collection." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip Core Endpoints +- **Testnet:** `http://platform.canary.enjin.io/graphql` +- **Mainnet:** `http://platform.enjin.io/graphql` +::: + +This is a detailed reference guide that explains the most commonly used operations. + +## Get Collection + +The `GetCollection` query allows you to retrieve detailed information about a specific collection by providing its `collectionId`. This information includes collection attributes, token details, and associated accounts. + +:::warning Reading Third-Party Collections +Please note that the Enjin Platform Cloud is set up to show only the collections and tokens that were created via the auth-ed Enjin Platform Cloud account.\ +To get a collection that was created elsewhere (via a different Enjin Platform Cloud account / [NFT.io](https://nft.io) / [Enjin Console](https://console.enjin.io) / etc.) the collection must be "Tracked" first, or the query response will return an error.\ +Learn more about tracking a collection in the [Tracking Collections section](/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md#tracking-collections). +::: + + + +```graphql +query GetCollection { + GetCollection(collectionId: 7153) { + collectionId + maxTokenCount + maxTokenSupply + forceSingleMint + frozen + network + owner { + account { + publicKey + address + } + } + attributes { + key + value + } + tokens { + edges { + cursor + node { + tokenId + } + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + accounts { + edges { + cursor + node { + accountCount + isFrozen + wallet { + account { + publicKey + address + } + } + approvals { + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } +} +``` + + +```json +{ + "data": { + "GetCollection": { + "collectionId": "7153", + "maxTokenCount": 100, + "maxTokenSupply": "100", + "forceSingleMint": false, + "frozen": false, + "network": "canary", + "owner": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "attributes": [], + "tokens": { + "edges": [ + { + "cursor": "eyJpZCI6NzI3MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "tokenId": "10" + } + }, + { + "cursor": "eyJpZCI6NzI3MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "tokenId": "6" + } + }, + { + "cursor": "eyJpZCI6Mzk0MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "tokenId": "68" + } + }, + { + "cursor": "eyJpZCI6Mzk1MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "tokenId": "70468841277235617716769448539773927607" + } + } + ], + "totalCount": 4, + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "startCursor": "", + "endCursor": "" + } + }, + "accounts": { + "edges": [ + { + "cursor": "eyJpZCI6MTM0OCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "accountCount": 2, + "isFrozen": false, + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "approvals": [] + } + }, + { + "cursor": "eyJpZCI6MjQ2MSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "accountCount": 2, + "isFrozen": false, + "wallet": { + "account": { + "publicKey": "0x985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050", + "address": "cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts" + } + }, + "approvals": [] + } + } + ], + "totalCount": 2, + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "startCursor": "", + "endCursor": "" + } + } + } + } +} +``` + + + +## GetCollections + +:::warning 🚧 Using the Enjin Platform Cloud? +The Enjin Platform Cloud has collections and tokens scoping enabled, to ensure a better experience by only showing you collections and tokens you created using the cloud.\ +If you wish to fetch collections/tokens created outside of your cloud account, consider transitioning to the [Enterprise On-Prem Enjin Platform](/05-enjin-platform/04-enterprise-on-prem/04-enterprise-on-prem.md). +::: + +The `GetCollections` query allows you to retrieve an array of collections. You can optionally filter the collections by providing a list of collection IDs that you are interested in. + + + +```graphql +query GetCollections { + GetCollections(collectionIds: [7153]) { + edges { + cursor + node { + collectionId + maxTokenCount + maxTokenSupply + forceSingleMint + frozen + network + owner { + account { + publicKey + address + } + } + attributes { + key + value + } + tokens { + edges { + cursor + node { + tokenId + } + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + accounts { + edges { + cursor + node { + accountCount + isFrozen + wallet { + account { + publicKey + address + } + } + approvals { + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + totalCount + } +} +``` + + +```json +{ + "data": { + "GetCollections": { + "edges": [ + { + "cursor": "eyJjb2xsZWN0aW9uX2lkIjo3MTUzLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "node": { + "collectionId": "7153", + "maxTokenCount": 100, + "maxTokenSupply": "100", + "forceSingleMint": false, + "frozen": false, + "network": "canary", + "owner": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "attributes": [], + "tokens": { + "edges": [ + { + "cursor": "eyJpZCI6NzI3MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "tokenId": "10" + } + }, + { + "cursor": "eyJpZCI6NzI3MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "tokenId": "6" + } + }, + { + "cursor": "eyJpZCI6Mzk0MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "tokenId": "68" + } + }, + { + "cursor": "eyJpZCI6Mzk1MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "tokenId": "70468841277235617716769448539773927607" + } + } + ], + "totalCount": 4, + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "startCursor": "", + "endCursor": "" + } + }, + "accounts": { + "edges": [ + { + "cursor": "eyJpZCI6MTM0OCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "accountCount": 2, + "isFrozen": false, + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "approvals": [] + } + }, + { + "cursor": "eyJpZCI6MjQ2MSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "accountCount": 2, + "isFrozen": false, + "wallet": { + "account": { + "publicKey": "0x985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050", + "address": "cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts" + } + }, + "approvals": [] + } + } + ], + "totalCount": 2, + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "startCursor": "", + "endCursor": "" + } + } + } + } + ], + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "startCursor": "", + "endCursor": "" + }, + "totalCount": 1 + } + } +} +``` + + \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/01-queries/03-tokens-queries.md b/versioned_docs/version-2.x/03-api-reference/01-queries/03-tokens-queries.md new file mode 100644 index 0000000..0a707ae --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/01-queries/03-tokens-queries.md @@ -0,0 +1,317 @@ +--- +title: "Tokens" +slug: "tokens" +description: "Query the Enjin API to retrieve detailed information about blockchain tokens, including ownership, metadata, and transaction history." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip Core Endpoints +- **Testnet:** `http://platform.canary.enjin.io/graphql` +- **Mainnet:** `http://platform.enjin.io/graphql` +::: + +This is a detailed reference guide that explains the most commonly used operations. + +## Get Token + +The `GetToken` query enables you to retrieve detailed information about a specific token from a collection. It requires the collectionId and tokenId parameters to access attributes, account details, and metadata related to the token. + +You can utilize the GetToken query for various purposes, including: + +- Retrieving Token Information: Get detailed information about a specific token, including supply, cap, and freeze status. +- Exploring Accounts: Explore accounts that hold the token, check their balances, and freeze status. +- Context of Collection: Obtain context about the collection to which the token belongs. +- Retrieving Attributes: Access attributes associated with the collection. +- Metadata Retrieval: Retrieve metadata associated with the token, such as name, color, or other descriptive information. + + + +```graphql +query GetToken { + GetToken( + collectionId: 7153 + tokenId: {integer:10} + ) { + tokenId + supply + cap + capSupply + isFrozen + minimumBalance + unitPrice + mintDeposit + attributeCount + nonFungible + metadata + collection { + collectionId + } + attributes { + key + value + } + accounts { + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + edges { + cursor + node { + balance + reservedBalance + isFrozen + wallet { + account { + publicKey + address + } + } + approvals { + amount + expiration + wallet { + account { + publicKey + address + } + } + } + namedReserves { + pallet + amount + } + } + } + } + } +} +``` + + +```json +{ + "data": { + "GetToken": { + "tokenId": "10", + "supply": "1", + "cap": "SINGLE_MINT", + "capSupply": null, + "isFrozen": false, + "minimumBalance": "1", + "unitPrice": "10000000000000000", + "mintDeposit": "20000000000000000", + "attributeCount": 0, + "nonFungible": true, + "metadata": null, + "collection": { + "collectionId": "7153" + }, + "attributes": [], + "accounts": { + "totalCount": 1, + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "startCursor": "", + "endCursor": "" + }, + "edges": [ + { + "cursor": "eyJpZCI6NzgxOCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "balance": "1", + "reservedBalance": "0", + "isFrozen": false, + "wallet": { + "account": { + "publicKey": "0x985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050", + "address": "cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts" + } + }, + "approvals": [], + "namedReserves": [] + } + } + ] + } + } + } +} +``` + + + +## GetTokens + +The `GetTokens` query allows you to retrieve an array of token data from a collection, making it suitable for batch retrieval and data analysis. You can specify filtering criteria to target specific tokens within the collection. + +You can utilize the `GetTokens` query for various purposes, including: + +- **Bulk Token Retrieval**: Retrieve information about multiple tokens within a collection, making it efficient for displaying token lists or galleries. +- **Filtered Token Retrieval**: Apply filters to obtain specific tokens based on criteria like token ID, collection, or attributes. +- **Marketplace Integration**: Fetch token data for use in a marketplace or trading platform to display available tokens and their details. +- **Data Analysis**: Analyze token data for insights, statistics, or reporting across a collection. +- **User Experience Enhancement**: Enhance the user experience by providing a comprehensive view of tokens within a collection. + + +:::warning Reading Third-Party Tokens +Please note that the Enjin Platform Cloud is set up to show only the collections and tokens that were created via the auth-ed Enjin Platform Cloud account.\ +To get a token that was created elsewhere (via a different Enjin Platform Cloud account / [NFT.io](https://nft.io) / [Enjin Console](https://console.enjin.io) / etc.) the collection must be "Tracked" first, or the query response will return an error.\ +Learn more about tracking a collection in the [Tracking Collections section](/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md#tracking-collections). +::: + + + +```graphql +query GetTokens { + GetTokens( + collectionId: 7153 + tokenIds: [{integer:10}] + ) { + edges { + cursor + node { + tokenId + supply + cap + capSupply + isFrozen + minimumBalance + unitPrice + mintDeposit + attributeCount + collection { + collectionId + } + attributes { + key + value + } + accounts { + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + edges { + cursor + node { + balance + reservedBalance + isFrozen + wallet { + account { + publicKey + address + } + } + approvals { + amount + expiration + wallet { + account { + publicKey + address + } + } + } + namedReserves { + pallet + amount + } + } + } + } + nonFungible + } + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } +} +``` + + +```json +{ + "data": { + "GetTokens": { + "edges": [ + { + "cursor": "eyJjb2xsZWN0aW9uX2lkIjo0OTk4LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "node": { + "tokenId": "10", + "supply": "1", + "cap": "SINGLE_MINT", + "capSupply": null, + "isFrozen": false, + "minimumBalance": "1", + "unitPrice": "10000000000000000", + "mintDeposit": "20000000000000000", + "attributeCount": 0, + "collection": { + "collectionId": "7153" + }, + "attributes": [], + "accounts": { + "totalCount": 1, + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "startCursor": "", + "endCursor": "" + }, + "edges": [ + { + "cursor": "eyJpZCI6NzgxOCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "balance": "1", + "reservedBalance": "0", + "isFrozen": false, + "wallet": { + "account": { + "publicKey": "0x985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050", + "address": "cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts" + } + }, + "approvals": [], + "namedReserves": [] + } + } + ] + }, + "nonFungible": true + } + } + ], + "totalCount": 1, + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "startCursor": "", + "endCursor": "" + } + } + } +} +``` + + \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/01-queries/04-wallets-queries.md b/versioned_docs/version-2.x/03-api-reference/01-queries/04-wallets-queries.md new file mode 100644 index 0000000..fd65501 --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/01-queries/04-wallets-queries.md @@ -0,0 +1,990 @@ +--- +title: "Wallets" +slug: "wallets" +description: "Use the Enjin API to perform wallets queries, including retrieving balances, transaction history, and associated blockchain assets." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip Core Endpoints +- **Testnet:** `http://platform.canary.enjin.io/graphql` +- **Mainnet:** `http://platform.enjin.io/graphql` +::: + +This is a detailed reference guide that explains the most commonly used operations. + +## GetWallet + +The `GetWallet` query provides detailed information about a specific wallet. This query retrieves various details related to the wallet, including account information, balances, associated collections and tokens, approvals, and transaction history. + + + +```graphql +query GetWallet { + GetWallet( + id: 5 + ) { + id + account { + publicKey + address + } + externalId + managed + network + nonce + balances { + free + reserved + miscFrozen + feeFrozen + } + collectionAccounts { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + accountCount + isFrozen + collection { + collectionId + } + wallet { + account { + publicKey + address + } + } + approvals { + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + } + tokenAccounts { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + balance + reservedBalance + isFrozen + collection { + collectionId + } + wallet { + account { + publicKey + address + } + } + token { + tokenId + } + approvals { + amount + expiration + wallet { + account { + publicKey + address + } + } + } + namedReserves { + pallet + amount + } + } + } + } + collectionAccountApprovals { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + tokenAccountApprovals { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + amount + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + transactions { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + transactionId + transactionHash + method + state + encodedData + events { + edges { + cursor + node { + phase + lookUp + moduleId + eventId + params { + type + value + } + } + } + totalCount + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + wallet { + account { + publicKey + address + } + } + } + } + } + ownedCollections { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + collectionId + maxTokenCount + maxTokenSupply + forceSingleMint + network + owner { + account { + publicKey + address + } + } + attributes { + key + value + } + accounts { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + accountCount + isFrozen + collection { + collectionId + } + wallet { + account { + publicKey + address + } + } + approvals { + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + } + tokens { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + tokenId + supply + cap + capSupply + isFrozen + minimumBalance + unitPrice + mintDeposit + attributeCount + collection { + collectionId + } + attributes { + key + value + } + accounts { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + balance + } + } + } + metadata + nonFungible + } + } + } + } + } + } + } +} +``` + + +```json +{ + "data": { + "GetWallet": { + "id": 5, + "account": { + "publicKey": "0x1edb67183fcd7846e717b16a37ae4326c7814090c748b5698a3b6a9e946a9f37", + "address": "cxK83gJm3F8LV4Kh1YjA6KaSgJev1RgatHrum4Ap4AFcHSrNe" + }, + "externalId": null, + "managed": false, + "network": "developer", + "nonce": 0, + "balances": { + "free": "0", + "reserved": "0", + "miscFrozen": "0", + "feeFrozen": "0" + }, + "collectionAccounts": { + "totalCount": 0, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasNextPage": false, + "hasPreviousPage": false + }, + "edges": [] + }, + "tokenAccounts": { + "totalCount": 0, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasNextPage": false, + "hasPreviousPage": false + }, + "edges": [] + }, + "collectionAccountApprovals": { + "totalCount": 0, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasNextPage": false, + "hasPreviousPage": false + }, + "edges": [] + }, + "tokenAccountApprovals": { + "totalCount": 0, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasNextPage": false, + "hasPreviousPage": false + }, + "edges": [] + }, + "transactions": { + "totalCount": 0, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasNextPage": false, + "hasPreviousPage": false + }, + "edges": [] + }, + "ownedCollections": { + "totalCount": 0, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasNextPage": false, + "hasPreviousPage": false + }, + "edges": [] + } + } + } +} +``` + + + +## GetWallets + +The `GetWallets` query allows you to retrieve an array of wallets. You have the option to apply filters based on parameters such as external IDs, verification IDs, or account addresses to narrow down the results. This query is useful for applications that need to access and display user wallet information, including transaction history and asset balances. + +:::warning This query might return items in multiple pages using Connections +To learn how to use GraphQL cursors for pagination, head to [Using the API → Pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination). +::: + + + +```graphql +query GetWallets { + GetWallets( + ids: [5] + ) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + account { + publicKey + address + } + externalId + managed + network + nonce + balances { + free + reserved + miscFrozen + feeFrozen + } + collectionAccounts { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + accountCount + isFrozen + collection { + collectionId + } + wallet { + account { + publicKey + address + } + } + approvals { + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + } + tokenAccounts { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + balance + reservedBalance + isFrozen + collection { + collectionId + } + wallet { + account { + publicKey + address + } + } + token { + tokenId + } + approvals { + amount + expiration + wallet { + account { + publicKey + address + } + } + } + namedReserves { + pallet + amount + } + } + } + } + collectionAccountApprovals { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + tokenAccountApprovals { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + amount + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + transactions { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + transactionId + transactionHash + method + state + encodedData + events { + edges { + cursor + node { + phase + lookUp + moduleId + eventId + params { + type + value + } + } + } + totalCount + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + wallet { + account { + publicKey + address + } + } + } + } + } + ownedCollections { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + collectionId + maxTokenCount + maxTokenSupply + forceSingleMint + network + owner { + account { + publicKey + address + } + } + attributes { + key + value + } + accounts { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + accountCount + isFrozen + collection { + collectionId + } + wallet { + account { + publicKey + address + } + } + approvals { + expiration + wallet { + account { + publicKey + address + } + } + } + } + } + } + tokens { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + tokenId + supply + cap + capSupply + isFrozen + minimumBalance + unitPrice + mintDeposit + attributeCount + collection { + collectionId + } + attributes { + key + value + } + accounts { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + balance + } + } + } + metadata + nonFungible + } + } + } + } + } + } + } + } + } +} +``` + + +```json +{ + "data": { + "GetWallets": { + "totalCount": 1, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasNextPage": false, + "hasPreviousPage": false + }, + "edges": [ + { + "cursor": "eyJpZCI6NSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "id": 5, + "account": { + "publicKey": "0x1edb67183fcd7846e717b16a37ae4326c7814090c748b5698a3b6a9e946a9f37", + "address": "cxK83gJm3F8LV4Kh1YjA6KaSgJev1RgatHrum4Ap4AFcHSrNe" + }, + "externalId": null, + "managed": false, + "network": "developer", + "nonce": 0, + "balances": { + "free": "0", + "reserved": "0", + "miscFrozen": "0", + "feeFrozen": "0" + }, + "collectionAccounts": { + "totalCount": 0, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasNextPage": false, + "hasPreviousPage": false + }, + "edges": [] + }, + "tokenAccounts": { + "totalCount": 0, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasNextPage": false, + "hasPreviousPage": false + }, + "edges": [] + }, + "collectionAccountApprovals": { + "totalCount": 0, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasNextPage": false, + "hasPreviousPage": false + }, + "edges": [] + }, + "tokenAccountApprovals": { + "totalCount": 0, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasNextPage": false, + "hasPreviousPage": false + }, + "edges": [] + }, + "transactions": { + "totalCount": 0, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasNextPage": false, + "hasPreviousPage": false + }, + "edges": [] + }, + "ownedCollections": { + "totalCount": 0, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasNextPage": false, + "hasPreviousPage": false + }, + "edges": [] + } + } + } + ] + } + } +} +``` + + + +## GetAccountVerified + +The `GetAccountVerified` query allows you to check the verification status of a specific account. It is used to determine whether an account is verified. The query takes a `verificationId` as a parameter, which serves as a unique identifier for the verification process. +The response will include whether the account is verified (`true` or `false`) and additional account information for reference. + + + +```graphql +query GetAccountVerified { + GetAccountVerified( + verificationId: "0x684768963b832de4f593da456ed3d099c2b8c580383492d3b0e735c4d1dfe740" + ) { + verified + account { + publicKey + address + } + } +} +``` + + +```json +{ + "data": { + "GetAccountVerified": { + "verified": true, + "account": { + "publicKey": "0x8b4cd0e710d3cd14b3a407cac1326dc6c8ac68b4ff22f8d239cbd460d831d608", + "address": "cxMaEYKfjJQrQQZ4cmXUjRqfPNRkRUSgsESMiEeyP6pzsBm4V" + } + } + } +} +``` + + + +## RequestAccount + +The `RequestAccount` query is designed for generating a QR code that users can scan to provide their wallet account information. It facilitates user authentication and linking wallet accounts to a service or application. The generated QR code contains encoded data that, when scanned, triggers a specific action, such as authentication or the transfer of wallet details. + + + +```graphql +query RequestAccount { + RequestAccount( + callback: "https://enjin.io" + ) { + qrCode + verificationId + } +} +``` + + +```json +{ + "data": { + "RequestAccount": { + "qrCode": "https://chart.googleapis.com/chart?chs=512x512&cht=qr&chl=https://deeplink.wallet.enjin.io/proof/0xa144ac5ef19bf4e3523cca27d9f998781c64692ea68843602da0dc95f6c4aa70:A2010LJAJ:aHR0cHM6Ly9lbmppbi5pbw==", + "verificationId": "0xa144ac5ef19bf4e3523cca27d9f998781c64692ea68843602da0dc95f6c4aa70" + } + } +} +``` + + + +## VerifyMessage + +The `VerifyMessage` query is used to verify the authenticity of a message by confirming that it was signed with a specific private key corresponding to the provided public key. This query is essential for ensuring the integrity and origin of messages in a secure communication or authentication process. + + + +```graphql +query VerifyMessage { + VerifyMessage( + message: "0x74657374" + signature: "0xbe32fc5fbe033ebb2dd94bcf0d06a52299042592006b0956cfd7606dea015b60f834d3b299172ad098e841efa3a61e9fb8e37a8912f85821eaf3df64f0e49083" + publicKey: "0xb0e3d2a901fe662e3f693704a599d4ec06da13b213f46e9ba479ba8e95dbfa61" + cryptoSignatureType: SR25519 + ) +} +``` + + +```json +{ + "data": { + "VerifyMessage": true + } +} +``` + + + +## GetPendingWallets + +The `GetPendingWallets` query is used to retrieve an array of wallet accounts that have not yet undergone the verification process. This query is particularly useful for administrators and account management systems to identify and handle pending wallet accounts that require verification. + + + +```graphql +query GetPendingWallets { + GetPendingWallets { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + account { + publicKey + address + } + externalId + managed + network + } + } + } +} +``` + + +```json +{ + "data": { + "GetPendingWallets": { + "totalCount": 0, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasNextPage": false, + "hasPreviousPage": false + }, + "edges": [] + } + } +} +``` + + diff --git a/versioned_docs/version-2.x/03-api-reference/01-queries/05-fuel-tank-queries.md b/versioned_docs/version-2.x/03-api-reference/01-queries/05-fuel-tank-queries.md new file mode 100644 index 0000000..8a9e7d8 --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/01-queries/05-fuel-tank-queries.md @@ -0,0 +1,283 @@ +--- +title: "Fuel Tanks" +slug: "fuel-tanks" +description: "Query the Enjin API for fuel tanks data, which helps cover transaction costs for users within the blockchain ecosystem." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip Fuel Tanks Endpoints +- **Testnet:** `http://platform.canary.enjin.io/graphql/fuel-tanks` +- **Mainnet:** `http://platform.enjin.io/graphql/fuel-tanks` +::: + +This is a detailed reference guide that explains the most commonly used operations. + +## GetAccounts + +The `GetAccounts` query allows you to retrieve a comprehensive list of account details associated with a specific fuel tank in the Enjin API. Fuel tanks are a feature used to subsidize transaction fees on the Enjin Blockchain. This query provides a detailed overview of all accounts participating in the specified fuel tank. + + + +```graphql +query GetAccounts { + GetAccounts(tankId: "cxNC9WmJa55k3EoXS3Fm4Bc69NaMGuY7qarchqPPn6jcWMA92") { + totalCount + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + } + edges { + node { + id + externalId + account { + publicKey + address + } + } + } + } +} +``` + + +```json +{ + "data": { + "GetFuelTanks": { + "totalCount": 1, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasPreviousPage": false, + "hasNextPage": false + }, + "edges": [ + { + "node": { + "name": "lhjb4xu2", + "providesDeposit": false, + "tankId": { + "publicKey": "0xa6b0ba89304439830fa19e0523ad8afa4180bb0960d35a80b7423836dd7b1276", + "address": "cxNC9WmJa55k3EoXS3Fm4Bc69NaMGuY7qarchqPPn6jcWMA92" + }, + "owner": { + "account": { + "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", + "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" + } + }, + "accounts": [ + { + "id": 8177, + "account": { + "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", + "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" + }, + "externalId": null + } + ], + "accountRules": [], + "dispatchRules": [] + } + } + ] + } + } +} +``` + + + +## GetFuelTank + +The `GetFuelTank` query allows you to retrieve detailed information about a specific fuel tank within the Enjin API. Fuel tanks are utilized for subsidizing transaction fees on the Enjin Blockchain. This query provides an extensive overview of the fuel tank's configuration, ownership, and associated rules. + + + +```graphql +query GetFuelTank { + GetFuelTank(tankId: "cxNC9WmJa55k3EoXS3Fm4Bc69NaMGuY7qarchqPPn6jcWMA92") { + name + providesDeposit + tankId { + publicKey + address + } + owner { + account { + publicKey + address + } + } + accounts { + id + account { + publicKey + address + } + externalId + } + accountRules { + rule + value + } + dispatchRules { + rule + value + isFrozen + } + } +} +``` + + +```json +{ + "data": { + "GetFuelTank": { + "name": "lhjb4xu2", + "providesDeposit": false, + "tankId": { + "publicKey": "0xa6b0ba89304439830fa19e0523ad8afa4180bb0960d35a80b7423836dd7b1276", + "address": "cxNC9WmJa55k3EoXS3Fm4Bc69NaMGuY7qarchqPPn6jcWMA92" + }, + "owner": { + "account": { + "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", + "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" + } + }, + "accounts": [ + { + "id": 8177, + "account": { + "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", + "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" + }, + "externalId": null + } + ], + "accountRules": [], + "dispatchRules": [] + } + } +} +``` + + + +## GetFuelTanks + +The `GetFuelTanks` query allows you to retrieve detailed information about multiple fuel tanks within the Enjin API simultaneously. This query is particularly useful when you need to obtain an overview or compare details of various fuel tanks within a system. + +:::warning This query might return items in multiple pages using Connections +To learn how to use GraphQL cursors for pagination, head to [Using the API → Pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination). +::: + + + +```graphql +query GetFuelTanks{ + GetFuelTanks(tankIds: ["0xa6b0ba89304439830fa19e0523ad8afa4180bb0960d35a80b7423836dd7b1276"]) { + totalCount + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + } + edges { + node { + name + providesDeposit + tankId { + publicKey + address + } + owner { + account { + publicKey + address + } + } + accounts { + id + account { + publicKey + address + } + externalId + } + accountRules { + rule + value + } + dispatchRules { + rule + value + isFrozen + } + } + } + } +} +``` + + +```json +{ + "data": { + "GetFuelTanks": { + "totalCount": 1, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasPreviousPage": false, + "hasNextPage": false + }, + "edges": [ + { + "node": { + "name": "lhjb4xu2", + "providesDeposit": false, + "tankId": { + "publicKey": "0xa6b0ba89304439830fa19e0523ad8afa4180bb0960d35a80b7423836dd7b1276", + "address": "cxNC9WmJa55k3EoXS3Fm4Bc69NaMGuY7qarchqPPn6jcWMA92" + }, + "owner": { + "account": { + "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", + "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" + } + }, + "accounts": [ + { + "id": 8177, + "account": { + "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", + "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" + }, + "externalId": null + } + ], + "accountRules": [], + "dispatchRules": [] + } + } + ] + } + } +} +``` + + \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/01-queries/06-marketplace-queries.md b/versioned_docs/version-2.x/03-api-reference/01-queries/06-marketplace-queries.md new file mode 100644 index 0000000..f18084f --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/01-queries/06-marketplace-queries.md @@ -0,0 +1,593 @@ +--- +title: "Marketplace" +slug: "marketplace" +description: "Query the Enjin API for marketplace data, including listing information, token trades, and transaction details." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip Marketplace Endpoints +- **Testnet:** `http://platform.canary.enjin.io/graphql/marketplace` +- **Mainnet:** `http://platform.enjin.io/graphql/marketplace` +::: + +This is a detailed reference guide that explains the most commonly used operations. + +## GetBid + +The `GetBid` query allows you to retrieve detailed information about a specific bid placed on a market listing. It's a valuable tool for querying bid-related data, including bid price, bidder details, and the associated market listing. + + +```graphql +query GetBid { + GetBid(id: 1) { + id + price + height + bidder { + account { + publicKey + address + } + } + listing { + listingId + } + } +} +``` + + +```json +{ + "data": { + "GetBid": { + "id": 1, + "price": "1000000000000000000", + "height": 239058, + "bidder": { + "account": { + "publicKey": "0xb4664455021025f4944c1bc4af8a2830317f0765dc624778345dea09e89a526a", + "address": "cxNW84hdDPEr8rU8oUbqNDaCmVX6SxH86mFufTf6HiNbZhi2F" + } + }, + "listing": { + "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20" + } + } + } +} +``` + + + +## GetBids + +The `GetBids` query is used to retrieve a list of bids for one or more market listings based on specified listing IDs. Unlike the `GetBid` query, which retrieves details of a single bid, `GetBids` can return multiple bids and includes pagination information. + + + +```graphql +query GetBids { + GetBids(listingIds: ["0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20"]) { + totalCount + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + } + edges { + node { + id + price + height + bidder { + account { + publicKey + address + } + } + listing { + listingId + } + } + } + } +} +``` + + +```json +{ + "data": { + "GetBids": { + "totalCount": 1, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasPreviousPage": false, + "hasNextPage": false + }, + "edges": [ + { + "node": { + "id": 1, + "price": "1000000000000000000", + "height": 239058, + "bidder": { + "account": { + "publicKey": "0xb4664455021025f4944c1bc4af8a2830317f0765dc624778345dea09e89a526a", + "address": "cxNW84hdDPEr8rU8oUbqNDaCmVX6SxH86mFufTf6HiNbZhi2F" + } + }, + "listing": { + "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20" + } + } + } + ] + } + } +} +``` + + + +## GetListing + +The `GetListing` query allows you to retrieve detailed information about a single market listing using its unique listing ID. This query provides a comprehensive overview of the listing's attributes, its state, sales history, bid history, and more. + + + +```graphql +query GetListing { + GetListing(listingId: "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20") { + listingId + makeAssetId { + collectionId + tokenId + } + takeAssetId { + collectionId + tokenId + } + amount + price + minTakeValue + feeSide + creationBlock + deposit + salt + state { + ... on FixedPriceState { + amountFilled + type + } + ... on AuctionState { + type + } + } + data { + ... on FixedPriceData { + type + } + ... on AuctionData { + type + startBlock + endBlock + } + } + seller { + account { + publicKey + address + } + } + sales { + edges { + node { + amount + price + bidder { + account { + publicKey + address + } + } + } + } + } + bids { + edges { + node { + price + bidder { + account { + publicKey + address + } + } + height + } + } + } + states { + state + height + } + } +} +``` + + +```json +{ + "data": { + "GetListing": { + "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20", + "makeAssetId": { + "collectionId": "4919", + "tokenId": "0" + }, + "takeAssetId": { + "collectionId": "0", + "tokenId": "0" + }, + "amount": "1", + "price": "1000000000000000000", + "minTakeValue": "975000000000000000", + "feeSide": "TAKE_FEE", + "creationBlock": 239058, + "deposit": "2025700000000000000", + "salt": "73616c74313233", + "state": { + "amountFilled": null, + "type": "FIXED_PRICE" + }, + "data": { + "type": "FIXED_PRICE" + }, + "seller": { + "account": { + "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", + "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" + } + }, + "sales": { + "edges": [] + }, + "bids": { + "edges": [ + { + "node": { + "price": "1000000000000000000", + "bidder": { + "account": { + "publicKey": "0xb4664455021025f4944c1bc4af8a2830317f0765dc624778345dea09e89a526a", + "address": "cxNW84hdDPEr8rU8oUbqNDaCmVX6SxH86mFufTf6HiNbZhi2F" + } + }, + "height": 239058 + } + } + ] + }, + "states": [ + { + "state": "ACTIVE", + "height": 239058 + } + ] + } + } +} +``` + + + +## GetListings + +The `GetListings` query is used to retrieve information about multiple market listings from the Enjin Blockchain marketplace. Unlike the `GetListing` query, which fetches data for a single listing, `GetListings` is designed to handle multiple listings at once, making it suitable for scenarios where you need to fetch details for multiple listings in a single request. + + + +```graphql +query GetListings { + GetListings( + listingIds: ["0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20"] + ) { + totalCount + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + } + edges { + node { + listingId + makeAssetId { + collectionId + tokenId + } + takeAssetId { + collectionId + tokenId + } + amount + price + minTakeValue + feeSide + creationBlock + deposit + salt + state { + ... on FixedPriceState { + amountFilled + type + } + ... on AuctionState { + type + } + } + data { + ... on FixedPriceData { + type + } + ... on AuctionData { + type + startBlock + endBlock + } + } + seller { + account { + publicKey + address + } + } + sales { + edges { + node { + amount + price + bidder { + account { + publicKey + address + } + } + } + } + } + bids { + edges { + node { + price + bidder { + account { + publicKey + address + } + } + height + } + } + } + states { + state + height + } + } + } + } +} +``` + + +```json +{ + "data": { + "GetListings": { + "totalCount": 1, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasPreviousPage": false, + "hasNextPage": false + }, + "edges": [ + { + "node": { + "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20", + "makeAssetId": { + "collectionId": "4919", + "tokenId": "0" + }, + "takeAssetId": { + "collectionId": "0", + "tokenId": "0" + }, + "amount": "1", + "price": "1000000000000000000", + "minTakeValue": "975000000000000000", + "feeSide": "TAKE_FEE", + "creationBlock": 239058, + "deposit": "2025700000000000000", + "salt": "73616c74313233", + "state": { + "amountFilled": null, + "type": "FIXED_PRICE" + }, + "data": { + "type": "FIXED_PRICE" + }, + "seller": { + "account": { + "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", + "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" + } + }, + "sales": { + "edges": [] + }, + "bids": { + "edges": [ + { + "node": { + "price": "1000000000000000000", + "bidder": { + "account": { + "publicKey": "0xb4664455021025f4944c1bc4af8a2830317f0765dc624778345dea09e89a526a", + "address": "cxNW84hdDPEr8rU8oUbqNDaCmVX6SxH86mFufTf6HiNbZhi2F" + } + }, + "height": 239058 + } + } + ] + }, + "states": [ + { + "state": "ACTIVE", + "height": 239058 + } + ] + } + } + ] + } + } +} +``` + + + +## GetSale + +The `GetSale` query is used to retrieve details about a specific sale transaction that has occurred on the Enjin Blockchain marketplace. This query requires the sale ID as input and returns information about the sale, including the sale ID, price, quantity, bidder information, and the associated market listing. + + + +```graphql +query GetSale { + GetSale(id: 1) { + id + price + amount + bidder { + account { + publicKey + address + } + } + listing { + listingId + } + } +} +``` + + +```json +{ + "data": { + "GetSale": { + "id": 1, + "price": "1000000", + "amount": "1", + "bidder": { + "account": { + "publicKey": "0x6a03b1a3d40d7e344dfb27157931b14b59fe2ff11d7352353321fe400e956802", + "address": "cxLpbEojWougf2cE6onB6PuK9SsfmKqNJKNo9tTsqirxFbuN5" + } + }, + "listing": { + "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20" + } + } + } +} +``` + + + +## GetSales + +The `GetSales` query is used to retrieve information about completed sale transactions that have occurred on a marketplace. This query allows you to fetch details of multiple sales associated with specific market listings. + + + +```graphql +query GetSales { + GetSales(listingIds: ["0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20"]) { + totalCount + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + } + edges { + node { + id + price + amount + bidder { + account { + publicKey + address + } + } + listing { + listingId + } + } + } + } +} +``` + + +```json +{ + "data": { + "GetSales": { + "totalCount": 1, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasPreviousPage": false, + "hasNextPage": false + }, + "edges": [ + { + "node": { + "id": 1, + "price": "1000000", + "amount": "1", + "bidder": { + "account": { + "publicKey": "0x6a03b1a3d40d7e344dfb27157931b14b59fe2ff11d7352353321fe400e956802", + "address": "cxLpbEojWougf2cE6onB6PuK9SsfmKqNJKNo9tTsqirxFbuN5" + } + }, + "listing": { + "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20" + } + } + } + ] + } + } +} +``` + + \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/01-queries/07-beam-queries.md b/versioned_docs/version-2.x/03-api-reference/01-queries/07-beam-queries.md new file mode 100644 index 0000000..b03aa05 --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/01-queries/07-beam-queries.md @@ -0,0 +1,613 @@ +--- +title: "Beam" +slug: "beam" +description: "Query the Enjin API for Beam data, including the status and details of QR code activations for blockchain assets distribution." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip Beam Endpoints +- **Testnet:** `http://platform.canary.enjin.io/graphql/beam` +- **Mainnet:** `http://platform.enjin.io/graphql/beam` +::: + +This is a detailed reference guide that explains the most commonly used operations. + +## GetBeam + +The `GetBeam` query retrieves detailed information about a specific . A Beam is a feature provided by Enjin that allows users to claim blockchain assets via QR codes. This query is essential for applications that need to access and display information about a particular Beam or track its usage. + + + +```graphql +query GetBeam { + GetBeam(code: "0dd3694f2c4599179f08686539cd73dc") { + id + code + name + description + image + start + end + isClaimable + claimsRemaining + qr { + url + payload + } + message { + walletPublicKey + message + } + collection { + collectionId + maxTokenCount + maxTokenSupply + forceSingleMint + frozen + network + } + } +} + +``` + + +```json +{ + "data": { + "GetBeam": { + "id": 1, + "code": "0dd3694f2c4599179f08686539cd73dc", + "name": "236", + "description": "This is the most awesome Beam ever.", + "image": "https://en.wikipedia.org/wiki/Firefox#/media/File:Firefox_logo,_2019.svg", + "start": "2022-11-14T00:00:00+00:00", + "end": "2028-11-14T00:00:00+00:00", + "isClaimable": true, + "claimsRemaining": 10, + "qr": { + "url": "https://platform.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc", + "payload": "https://platform.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc" + }, + "message": null, + "collection": null + } + } +} +``` + + + +## GetBeams + +The `GetBeams` query is used to retrieve an array of beam details, optionally filtered by codes or names. This query is particularly useful for applications that need to fetch information about multiple Beams at once, making it easier to manage and display Beam data. + + + +```graphql +query GetBeams { + GetBeams(codes: ["0dd3694f2c4599179f08686539cd73dc"]) { + totalCount + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + node { + id + code + name + description + image + start + end + isClaimable + qr { + url + payload + } + collection { + collectionId + maxTokenCount + maxTokenSupply + forceSingleMint + frozen + network + } + claims { + totalCount + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + node { + id + claimedAt + claimStatus + quantity + code + wallet { + account { + publicKey + address + } + } + collection { + collectionId + maxTokenCount + maxTokenSupply + forceSingleMint + frozen + network + } + } + } + } + } + } + } +} +``` + + +```json +{ + "data": { + "GetBeams": { + "totalCount": 1, + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": "", + "endCursor": "" + }, + "edges": [ + { + "node": { + "id": 1, + "code": "0dd3694f2c4599179f08686539cd73dc", + "name": "236", + "description": "This is the most awesome Beam ever.", + "image": "https://en.wikipedia.org/wiki/Firefox#/media/File:Firefox_logo,_2019.svg", + "start": "2022-11-14T00:00:00+00:00", + "end": "2028-11-14T00:00:00+00:00", + "isClaimable": true, + "qr": { + "url": "https://platform.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc", + "payload": "https://platform.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc" + }, + "collection": null, + "claims": { + "totalCount": 10, + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": "", + "endCursor": "" + }, + "edges": [ + { + "node": { + "id": 1, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "code": "eyJpdiI6IllGcEg0L0lRL0x5c1BQeWIyVXdCL0E9PSIsInZhbHVlIjoiVmhtcTRqcUhoeFBPTGQ2QTNWOTQ5aWhOR3poVlJrOFo0QTVHeUpVSUZWcTFwaGR1YW1KMmN5YWJLZ2xKbW1WSSIsIm1hYyI6ImRiMGIyMTAyNjEwOTNkNWUwNTUxZWVkMDhlZGEyZjQ2YmQwMGY5MzhhYjA5ZjBmZmE4MDk4NmNiYTkzM2FiNWQiLCJ0YWciOiIifQ==", + "wallet": null, + "collection": null + } + }, + { + "node": { + "id": 2, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "code": "eyJpdiI6ImllVllKclNObjk4QXdtZnRjVitLWmc9PSIsInZhbHVlIjoiQlozZzVQSHZWOTZibWxGSkkzcWxBMVNiMkFaZGcxcERGNitBOE1sMFVwZFJsdkE5OXBMVlNlRVZxOGN4eGZaRyIsIm1hYyI6IjI1NzQ5ZjRkOGMzMzBjMWRkNjY1YWZhYjllYjliYmE0OThmYmQ1MGFiMjhhYjhiNTA0ZDlkYWM2OTU5YzJmN2YiLCJ0YWciOiIifQ==", + "wallet": null, + "collection": null + } + }, + { + "node": { + "id": 3, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "code": "eyJpdiI6IjZYdUVWeTYwK0czRjM2THNHNVBJWFE9PSIsInZhbHVlIjoialhHbS93YkhnNldJa2JHeDFBMCt2V3ZLRDZkVi9INXgvdG1sOGxhSTJwWk12RE1HMEN2ZFZyY2d3dzRDQ0loNCIsIm1hYyI6IjVjOGE0ZGEyYzUzYWQzODgwZDJkNmNlZjM4YjY0YmZmOTcwNTVhNGIyNzUzYWNlYjMxOWNiNGE5OWZhY2NiNmYiLCJ0YWciOiIifQ==", + "wallet": null, + "collection": null + } + }, + { + "node": { + "id": 4, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "code": "eyJpdiI6Ik9TeDE3ZGMxODFHdXRwUTM4QXp3OUE9PSIsInZhbHVlIjoiaC9mUGtjTkd5RjNFdEk4WGFnNW5QOWJhVFVBR3ZrOXE3bTMrTm9GT01SVVV5NVpVRSt2RGtnQndnTld3TFRlZyIsIm1hYyI6IjdhNjgzYTJhMWQ2NzZlZWM1NTIyZWVkNjFkNzNjNzNlN2JhNmMyZTAyYzhmYTE2M2M1NzNlMTAxNTNlMDI3OGQiLCJ0YWciOiIifQ==", + "wallet": null, + "collection": null + } + }, + { + "node": { + "id": 5, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "code": "eyJpdiI6IlhYVVF0c0l0ZDNzS2g3a0hLU3JaeVE9PSIsInZhbHVlIjoiWWltZjlvMjJ1M0psRHJPUG1QRmUvVlZnR2RiREQyTWxYVU9pSUtNb2N2MWxqbzFmTVYxM0xiV3cxZWJ4NFg4TyIsIm1hYyI6IjdlOTExYTg5OTIwMDMxMTZkYmJlNGYwZjU5MjhkOWJlMzgyZWVhYzcyMDEwYzRkNzk5Zjk2OWU5YjZjODExNmYiLCJ0YWciOiIifQ==", + "wallet": null, + "collection": null + } + }, + { + "node": { + "id": 6, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "code": "eyJpdiI6IlVlUjBKeUhrZ092clBqMDVNaTNwOXc9PSIsInZhbHVlIjoiOUNhcUxaQkVDVlZFdnlBM3ZtTUJmWnl0bjN6YVNuUmRXbmoxWXZiOFpZOUFqaW9Od2Z0ZGdWMjF0S2llRHZEUyIsIm1hYyI6Ijg0YjY1ZWEzZGFjOWE5NjI2OTIwMjExNzc2NTNhZWZiMDI3OGQwZGQyZDEzYWJhMjg2OGExZWQyMzJiMmJlM2EiLCJ0YWciOiIifQ==", + "wallet": null, + "collection": null + } + }, + { + "node": { + "id": 7, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "code": "eyJpdiI6InAxU1I4QzlXcGp2RXRuSENCTkVzcnc9PSIsInZhbHVlIjoiL2J2a0UyK0trd0pJMVJ0QVI0NU9TanFuZmVCcUY3dG5TNW50YURzbXdwUkhBSVIyQTlnQnZHK0trTjh1am9pbiIsIm1hYyI6ImMzYzVkZDZmOTQ2OWRiODBmOTMwMWJkYzZhZTE0ZTkwZTczOGFlZjJkMmYzOTQzZjA1ZGMxNjExMmU5MjRiYjQiLCJ0YWciOiIifQ==", + "wallet": null, + "collection": null + } + }, + { + "node": { + "id": 8, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "code": "eyJpdiI6InVaRWlMYk1HSjdkZFRhWVlGQlRMK3c9PSIsInZhbHVlIjoidUJjTWpPODZRMThNeUF1T0hHaWJlUTB0a0thTXl5VnZFSHN4UjYzQk5uZ1NQR0lrNWR1NlY1VG5sU2cyZ0R6diIsIm1hYyI6ImZiZjhkY2JhOTY3MmUwZGI2YzYyZmQ4MGFkYmVjMzI2NzRiYWZmOWI4ZjgzMjNkYTRlZTc5MmEyNzZmN2E0MzciLCJ0YWciOiIifQ==", + "wallet": null, + "collection": null + } + }, + { + "node": { + "id": 9, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "code": "eyJpdiI6Ik14U2ZndlJiLzdsQktMckdQTGQxeHc9PSIsInZhbHVlIjoiUDBEWGNYcDk1ZExlZlMzNzRPWEdFeFliYmIwWWIxUElzaEpHSkQxb05xN2EyVVVucnd2dWQzTC9Ic3JzYnlSeSIsIm1hYyI6ImY4ODI0OGU0Mzc3NjVkMzZlODljYTI2YzhkNDU3N2MyMmNiZTM0YjZkMWQ3YjU2NjE0NmZlZjkyYjk0NjllOGQiLCJ0YWciOiIifQ==", + "wallet": null, + "collection": null + } + }, + { + "node": { + "id": 10, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "code": "eyJpdiI6InhiYWZqWlM3YTRhc01EYXZaR2pwRFE9PSIsInZhbHVlIjoiZXlwVytJbWNRT29VOThrR28wWldBa1l3Y3ZPMW5oUmdwUE02Snh1M2FDek4wOXpsS1FEVEFjNU9GV2psNWZCMyIsIm1hYyI6ImI3NTU3YTJjNWQzODQyNjllOGJmZTA5MjBlMjBhYTI3ZTA1Yjg1MmYzN2RiOWQ2MDJjMGRlMmY3Y2YxMDk2ODkiLCJ0YWciOiIifQ==", + "wallet": null, + "collection": null + } + } + ] + } + } + } + ] + } + } +} +``` + + + +## GetClaims + +The `GetClaims` query is used to retrieve an array of claim details from a system, and it allows for optional filtering based on claim IDs, codes, accounts, or states. This query is typically used to track and manage claims associated with specific assets or codes in a system. + + + +```graphql +query GetClaims { + GetClaims( + codes: ["0dd3694f2c4599179f08686539cd73dc"] + ) { + edges { + cursor + node { + id + claimedAt + claimStatus + quantity + wallet { + account { + publicKey + address + } + } + collection { + collectionId + maxTokenCount + maxTokenSupply + forceSingleMint + frozen + network + } + beam { + id + code + } + } + } + totalCount + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + } + } +} +``` + + +```json +{ + "data": { + "GetClaims": { + "edges": [ + { + "cursor": "eyJpZCI6MTAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "id": 10, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "wallet": null, + "collection": null, + "beam": { + "id": 1, + "code": "0dd3694f2c4599179f08686539cd73dc" + } + } + }, + { + "cursor": "eyJpZCI6OSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "id": 9, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "wallet": null, + "collection": null, + "beam": { + "id": 1, + "code": "0dd3694f2c4599179f08686539cd73dc" + } + } + }, + { + "cursor": "eyJpZCI6OCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "id": 8, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "wallet": null, + "collection": null, + "beam": { + "id": 1, + "code": "0dd3694f2c4599179f08686539cd73dc" + } + } + }, + { + "cursor": "eyJpZCI6NywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "id": 7, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "wallet": null, + "collection": null, + "beam": { + "id": 1, + "code": "0dd3694f2c4599179f08686539cd73dc" + } + } + }, + { + "cursor": "eyJpZCI6NiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "id": 6, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "wallet": null, + "collection": null, + "beam": { + "id": 1, + "code": "0dd3694f2c4599179f08686539cd73dc" + } + } + }, + { + "cursor": "eyJpZCI6NSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "id": 5, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "wallet": null, + "collection": null, + "beam": { + "id": 1, + "code": "0dd3694f2c4599179f08686539cd73dc" + } + } + }, + { + "cursor": "eyJpZCI6NCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "id": 4, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "wallet": null, + "collection": null, + "beam": { + "id": 1, + "code": "0dd3694f2c4599179f08686539cd73dc" + } + } + }, + { + "cursor": "eyJpZCI6MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "id": 3, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "wallet": null, + "collection": null, + "beam": { + "id": 1, + "code": "0dd3694f2c4599179f08686539cd73dc" + } + } + }, + { + "cursor": "eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "id": 2, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "wallet": null, + "collection": null, + "beam": { + "id": 1, + "code": "0dd3694f2c4599179f08686539cd73dc" + } + } + }, + { + "cursor": "eyJpZCI6MSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", + "node": { + "id": 1, + "claimedAt": null, + "claimStatus": null, + "quantity": 10, + "wallet": null, + "collection": null, + "beam": { + "id": 1, + "code": "0dd3694f2c4599179f08686539cd73dc" + } + } + } + ], + "totalCount": 10, + "pageInfo": { + "startCursor": "", + "endCursor": "", + "hasPreviousPage": false, + "hasNextPage": false + } + } + } +} +``` + + + +## GetSingleUseCodes + +The `GetClaims` query retrieves an array of claim details, optionally filtered by claim IDs, claim codes, accounts, or claim states. It is designed to provide information about claims associated with a specific code. + + + +```graphql +query GetSingleUseCodes { + GetSingleUseCodes(code: "7cd390ab3d56c42b818d0a1aa2221c8f") { + edges { + cursor + node { + code + qr { + url + payload + } + } + } + totalCount + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } +} +``` + + +```json +{ + "data": { + "GetSingleUseCodes": { + "edges": [ + { + "cursor": "eyJpZCI6ODAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "code": "eyJpdiI6IlNFM2tRZzgxaU5pZi9hVXBVZTBCeVE9PSIsInZhbHVlIjoicDdsSmlCU1JHNnd6QUtHU1FRV2pNQ2twaDNPT2YreFVrK3Jrb1VXL0NPc2tOeFBVdTZFM2J1Ykp5NTlRNlNwZXg0d1JTbHo1N1BHMDJqaG11WDhDTGIxRVlVenBoTWVwT3ZrL3RwR2pZTzA9IiwibWFjIjoiYWI3ZjdhY2RmMzE2NGM1MjAxMWVmYTk2ZWNjNWQyNTY4MWQ4NGZhY2ZhN2M5Y2FmMmM0MzU5MDM1N2Q1NDFiYSIsInRhZyI6IiJ9", + "qr": { + "url": "https://platform.enjin.io/claim/eyJpdiI6ImVMNTEybWEwSnZ6WlF3QjUvQW15Wmc9PSIsInZhbHVlIjoiVklOcCtCYVpGTG1aRjQyY0FZb3RmS1JSd1JGT0dka05rT1NQUnZoMXZ4UGsxVklzVmNoejhEUDQ5TjVMc0picjR4MmEzQUJTaWMrUVYxb21yMExZRWVjUGp1T2Rnb0owMHcwWmxXWCtGMnM9IiwibWFjIjoiMjUyNzdlODkyMTRkZmViM2M1OWY5MWNlYjgxYjUzOGE3NTA4NmU0NjY3MjIwOGY0ZDkxNTg3ZTJjOWM5NjUxMSIsInRhZyI6IiJ9", + "payload": "https://platform.enjin.io/claim/eyJpdiI6IkJJWVNYb3lFVGRpaUNEemNJQ20xamc9PSIsInZhbHVlIjoic1ZXMzNGN1FnbWVRc1Z2S2IrYThIT0Rpd2JpeGhza2wxSG1FSlVqTUhzN3UrYmozaXNnZ2dSTU5HTmluZ2laY3JWNUcyemcyaDJZT1VGdGx6SjgxdGVwdUIvN1JhRHRVSnd2RFRYZVhOOUk9IiwibWFjIjoiNzllYmQ1MjFlODBmMWEzOTdjOTc0YWM0YjQ4M2ExZmEyOWZhNzQyY2RlNGJjMzcxNDdkOTQxOWJlYmYyMmUwMSIsInRhZyI6IiJ9" + } + } + }, + { + "cursor": "eyJpZCI6NzEsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", + "node": { + "code": "eyJpdiI6Imp5R0xzNC9aVWZlT090SGVsL3RyVlE9PSIsInZhbHVlIjoiemJiWmsrZEl2TXVzc2R1c2svalhIRjdGVUFXRThPOTg2RnZJVCtMNEE1UENxN3FLeURhL1J4UXhBakduekduRmxRQTJwdmRCeXZqQi9rbVVyVnRmK2V4c2xDZU9jUW15N0xNSkRiUyt2ZlE9IiwibWFjIjoiNTZlOGQ4NjdmNDNmOTQ3NWYwM2M2MzRlMGRmODlmNmQwZmM2ZDllNjdmMTk4ZTA0ZWJiYTliZTZkZTJhOTM3MyIsInRhZyI6IiJ9", + "qr": { + "url": "https://platform.enjin.io/claim/eyJpdiI6IlhtTHdEdlM5bXlQNnVEaGZpejkrR3c9PSIsInZhbHVlIjoidlh3dVhuTVhOVEQvcElBM0drMVhBQ2Z1SFdob3BtNmJnYVQwWExoVmlUYWdPTUhkUzJ1QS9JL25yeHVPSnBQb2VyM3lJWXd3aEMySVd1MjkwellTdW50UVl1YTJRdmNCV3lzdXVLaE9Wckk9IiwibWFjIjoiOGNhNmY1MzZlZWRjMmE0ZjMwYjQ0ODY0Nzg5MTk5MDU4OTAwYmZhYjYzODI4OGQ3ZDIyNTQ5MjA2Zjc2MTczYiIsInRhZyI6IiJ9", + "payload": "https://platform.enjin.io/claim/eyJpdiI6IkN4OHZzUDdGc1AwUG9wdXpQSDVGYXc9PSIsInZhbHVlIjoiOTJJRHQ3VjdoZHR0WVpqajVoSGZIZFNoZHJtVE1iRjR0ZndLQXRQYVE1TS8zaGd1WEZDdDQrckNzUXExdnFvZDVVcE5PMVU1bklFRWhqSnVXVi96WVB4RE5CMk1qTFhHYVIvVWdscFljUTA9IiwibWFjIjoiYWIyY2U4YmIzMjJmNjY0ZmE2NzZjMTZhYTE0YjJlMTEwNDQxMjJlY2Y3NzZlYzZkMDNhMGI3NGQ2MDEyYTZlZSIsInRhZyI6IiJ9" + } + } + } + ], + "totalCount": 10, + "pageInfo": { + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": "", + "endCursor": "" + } + } + } +} +``` + + \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/01-queries/_category_.json b/versioned_docs/version-2.x/03-api-reference/01-queries/_category_.json new file mode 100644 index 0000000..fa343b5 --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/01-queries/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Queries (Read)" +} + \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/02-mutations/01-transaction-mutations.md b/versioned_docs/version-2.x/03-api-reference/02-mutations/01-transaction-mutations.md new file mode 100644 index 0000000..f1c3b72 --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/02-mutations/01-transaction-mutations.md @@ -0,0 +1,94 @@ +--- +title: "Transactions" +slug: "transactions" +description: "Use the Enjin API to perform transactions mutations, enabling the creation and modification of blockchain transactions, including asset transfers." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip Core Endpoints +- **Testnet:** `http://platform.canary.enjin.io/graphql` +- **Mainnet:** `http://platform.enjin.io/graphql` +::: + +This is an overview of some of the most commonly used operations in the Enjin Platform Schema. + +## AcknowledgeEvents + +The `AcknowledgeEvents` mutation allows you to confirm the processing of specific events by acknowledging them and removing them from the cache. This helps maintain cache integrity and ensures efficient system operation by eliminating unnecessary data. + + + +```graphql +mutation AcknowledgeEvents{ + AcknowledgeEvents( + uuids: ["25f506ef-92a1-40a9-b034-60f9ab5045da"] + ) +} +``` + + +```json +{ + "data": { + "AcknowledgeEvents": true + } +} +``` + + + +## UpdateTransaction + +The `UpdateTransaction` mutation enables you to update an existing transaction with new information. This is useful for maintaining accurate transaction records and tracking the lifecycle of transactions. + + + +```graphql +mutation UpdateTransaction{ + UpdateTransaction( + id: 7 + signedAtBlock: 320100 + ) +} +``` + + +```json +{ + "data": { + "UpdateTransaction": true + } +} +``` + + + +## RetryTransactions + +The `RetryTransactions` mutation provides a mechanism to retry transactions that have previously failed. This is particularly useful for addressing transaction failures caused by temporary issues such as network congestion or gas price spikes. + + + +```graphql +mutation RetryTransactions{ + RetryTransactions(ids: [175]) +} +``` + + +```json +{ + "data": { + "RetryTransactions": true + } +} +``` + + \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/02-mutations/02-collections-mutations.md b/versioned_docs/version-2.x/03-api-reference/02-mutations/02-collections-mutations.md new file mode 100644 index 0000000..bb51eae --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/02-mutations/02-collections-mutations.md @@ -0,0 +1,495 @@ +--- +title: "Collections" +slug: "collections" +description: "Perform collections mutations in the Enjin API to manage your blockchain collections, including creating, modifying, and updating assets." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip Core Endpoints +- **Testnet:** `http://platform.canary.enjin.io/graphql` +- **Mainnet:** `http://platform.enjin.io/graphql` +::: + +This is a detailed reference guide that explains the most commonly used operations. + +## CreateCollection + +The `CreateCollection` mutation is used to create a new on-chain collection. A collection serves as a grouping of on-chain assets, typically non-fungible tokens (NFTs), that share common properties or belong to the same set. + + + +```graphql +mutation CreateCollection { + CreateCollection{ + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} +``` + + +```json +{ + "data": { + "CreateCollection": { + "id": 14058, + "transactionId": null, + "transactionHash": null, + "method": "CreateCollection", + "state": "PENDING", + "encodedData": "0x2800000001000000", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "idempotencyKey": "ef403ad0-7b6c-4020-8ef1-01b1dc6f4513" + } + } +} +``` + + + +## ApproveCollection + +The `ApproveCollection` mutation is used to authorize another account (referred to as the "operator") to transfer tokens from a specific collection account. This operation is common in scenarios involving Non-Fungible Tokens (NFTs) or tokenized assets, where token management may need to be delegated without transferring ownership of the tokens. + + + +```graphql +mutation ApproveCollection { + ApproveCollection( + collectionId: "7154" + operator: "0x965bcdbb46614cbd79869e2eb568825f6c038cbdf9085edb1b164607d3738fa6" + expiration: 445100 + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "ApproveCollection": { + "id": 13819, + "transactionId": null, + "transactionHash": null, + "method": "ApproveCollection", + "state": "PENDING", + "encodedData": "0x280fc96f965bcdbb46614cbd79869e2eb568825f6c038cbdf9085edb1b164607d3738fa601acca0600", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## UnapproveCollection + +The `UnapproveCollection` mutation is used to revoke previously granted permissions for a specific account (referred to as the "operator") to transfer items from a collection owned by the sender's account. This operation is essential for enhancing the security and control of digital assets, ensuring that the owner of a collection can manage who has the authority to move or transfer items from their collection. + + + +```graphql +mutation UnapproveCollection{ + UnapproveCollection( + collectionId: "6305" + operator: "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "UnapproveCollection": { + "id": 14078, + "transactionId": null, + "transactionHash": null, + "method": "UnapproveCollection", + "state": "PENDING", + "encodedData": "0x28108562d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## MutateCollection + +The `MutateCollection` mutation is utilized to modify the default settings of an existing collection. This mutation allows you to make changes to various parameters of a collection, such as adjusting royalty settings. It is particularly valuable when you need to alter how royalties are distributed for the assets within a specific collection. + + + +```graphql +mutation MutateCollection { + MutateCollection( + collectionId: "10943" + mutation: { + royalty: { + beneficiary:"0x50a1ba0a184c9aca3a2ac7d427e96a676fe988454b4b56a62dd6622e843e890d" + percentage: 50 + } + } + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "MutateCollection": { + "id": 14066, + "transactionId": null, + "transactionHash": null, + "method": "MutateCollection", + "state": "PENDING", + "encodedData": "0x2802fdaa00010150a1ba0a184c9aca3a2ac7d427e96a676fe988454b4b56a62dd6622e843e890d0294357700", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## Burn + +The `Burn` mutation is used to permanently delete a specified amount of tokens from a collection. This operation is irreversible and results in the removal of tokens from circulation. When tokens are burned, the reserved value associated with them is often returned to the issuer's account. + + + +```graphql +mutation Burn { + Burn( + collectionId: "7154" + params: { + tokenId: {integer:6533} + amount:1 + keepAlive:false + } + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "Burn": { + "id": 14055, + "transactionId": null, + "transactionHash": null, + "method": "Burn", + "state": "PENDING", + "encodedData": "0x2805c96f1566040000", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## DestroyCollection + +The `DestroyCollection` mutation is used to permanently delete an existing collection. This operation is irreversible and removes the entire collection, including all associated tokens and their reserved values. + + + +```graphql +mutation DestroyCollection{ + DestroyCollection(collectionId: 10942) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "DestroyCollection": { + "id": 14062, + "transactionId": null, + "transactionHash": null, + "method": "DestroyCollection", + "state": "PENDING", + "encodedData": "0x2801f9aa", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## Freeze + +The `Freeze` mutation is used to temporarily halt token transfers on a collection, token, collection account, or token account. Freezing temporarily prevents token transfers from taking place, which also results in marketplace listings being unpurchaseable. + + + +```graphql +mutation Freeze { + Freeze( + freezeType: COLLECTION + collectionId: "10943" + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "Freeze": { + "id": 14064, + "transactionId": null, + "transactionHash": null, + "method": "Freeze", + "state": "PENDING", + "encodedData": "0x2807fdaa00", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## SetCollectionAttribute + +The `SetCollectionAttribute` mutation allows you to assign a new attribute or update an existing attribute's value within a collection. + + + +```graphql +mutation SetCollectionAttribute { + SetCollectionAttribute( + collectionId: "4741" + key: "test" + value: "Hello" + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "SetCollectionAttribute": { + "id": 14072, + "transactionId": null, + "transactionHash": null, + "method": "SetCollectionAttribute", + "state": "PENDING", + "encodedData": "0x2809154a0010746573741448656c6c6f", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## RemoveCollectionAttribute + +The `RemoveCollectionAttribute` mutation is designed to remove a specific attribute from an entire collection of tokens. Attributes typically represent metadata associated with the tokens, such as name, color, size, or any other descriptive information. + + + +```graphql +mutation RemoveCollectionAttribute { + RemoveCollectionAttribute( + collectionId: "4741" + key: "name" + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "RemoveCollectionAttribute": { + "id": 14070, + "transactionId": null, + "transactionHash": null, + "method": "RemoveCollectionAttribute", + "state": "PENDING", + "encodedData": "0x280a154a00106e616d65", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/02-mutations/03-tokens-mutations.md b/versioned_docs/version-2.x/03-api-reference/02-mutations/03-tokens-mutations.md new file mode 100644 index 0000000..f45639e --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/02-mutations/03-tokens-mutations.md @@ -0,0 +1,1033 @@ +--- +title: "Tokens" +slug: "tokens" +description: "Use the Enjin API to perform tokens mutations, including minting new tokens, burning existing ones, and modifying token metadata." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip Core Endpoints +- **Testnet:** `http://platform.canary.enjin.io/graphql` +- **Mainnet:** `http://platform.enjin.io/graphql` +::: + +This is a detailed reference guide that explains the most commonly used operations. + +## CreateToken + +The `CreateToken` mutation enables you to create a new token within an existing collection. This operation is essential for introducing new digital assets, and it allows you to define various attributes and characteristics for the newly created token. + + + +```graphql +mutation CreateToken { + CreateToken( + recipient: "0xc4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3" + collectionId: "10941" + params: { + tokenId:{integer:1} + initialSupply:10 + cap: { + type:INFINITE + } + metadata: {name: "Gold Coins", symbol: "GOLD", decimalCount: 2} + } + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "CreateToken": { + "id": 14060, + "transactionId": null, + "transactionHash": null, + "method": "CreateToken", + "state": "PENDING", + "encodedData": "0x280400c4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3f5aa00042800010000c16ff28623000000000000000000000000000000", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +:::info +For Token ID management, head to [Advamced Mechanics → TokenID Structure](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md) +::: + +## MintToken + +The `MintToken` mutation allows you to increase the supply of an existing token. This operation is essential for managing the circulating supply of tokens and is typically used when you need to create additional units of a specific token. + + + +```graphql +mutation MintToken { + MintToken( + recipient: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" + collectionId: "7154" + params: { + amount:1 + tokenId: {integer: 6533} + } + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "MintToken": { + "id": 14065, + "transactionId": null, + "transactionHash": null, + "method": "MintToken", + "state": "PENDING", + "encodedData": "0x280400aa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921c96f01156604010000c16ff28623000000000000000000", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## BatchMint + +The `BatchMint` mutation enables you to efficiently create multiple tokens within a single blockchain transaction. This process, known as batch minting, simplifies the minting of multiple tokens, reducing transaction fees and processing time. The maximum number of recipients that you can batch into a single transaction is 250. + + + +```graphql +mutation BatchMint { + BatchMint( + collectionId: "7154" + recipients: [ + { + account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" + mintParams: { + amount:1 + tokenId: {integer: 6533} + } + }, + { + account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7215a17f847" + mintParams: { + amount:5 + tokenId: {integer: 4422} + } + } + ] + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "BatchMint": { + "id": 13928, + "transactionId": null, + "transactionHash": null, + "method": "BatchMint", + "state": "PENDING", + "encodedData": "0x280dc96f04aa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c292101156604010000c16ff28623000000000000000000", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## SetTokenAttribute + +The `SetTokenAttribute` mutation allows you to assign or update attributes for a specific token within a collection. It is used to manage individual token metadata and properties. + + + +```graphql +mutation SetTokenAttribute{ + SetTokenAttribute( + collectionId: "4741" + tokenId: {integer:0} + key: "test" + value: "Hello" + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "SetTokenAttribute": { + "id": 14073, + "transactionId": null, + "transactionHash": null, + "method": "SetTokenAttribute", + "state": "PENDING", + "encodedData": "0x2809154a010000000000000000000000000000000010746573741448656c6c6f", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## BatchSetAttribute + +The `BatchSetAttribute` mutation allows you to efficiently set or update multiple attributes for a specific token within a collection in a single blockchain transaction. Attributes represent various properties, characteristics, or metadata associated with a token. + + + +```graphql +mutation BatchSetAttribute { + BatchSetAttribute( + collectionId: "7154" + tokenId: {integer:6533} + attributes: [ + { + key:"Name" + value:"John" + }, + { + key:"Surname" + value:"Snow" + }, + { + key:"House" + value:"Stark" + } + ] + ) { + id + transactionId + transactionHash + method + encodedData + state + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "BatchSetAttribute": { + "id": 13961, + "transactionId": null, + "transactionHash": null, + "method": "BatchSetAttribute", + "encodedData": "0x280ec96f01851900000000000000000000000000000410746573741468656c6c6f", + "state": "PENDING", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## BatchTransfer + +The `BatchTransfer` mutation streamlines the process of transferring multiple tokens to one or more recipients within a single blockchain transaction. This is particularly useful when you need to distribute tokens to various accounts, such as rewarding multiple users or dispersing assets to different stakeholders. + + + +```graphql +mutation BatchTransfer { + BatchTransfer( + collectionId: 36105 + recipients: [ + { + account:"cxM1YqYERiNvuEKkZUiUFefCtTfdy2q4Bj7QuWDfSPJuiuDWy" + simpleParams: { + tokenId: {integer:1} + amount:1 + } + }, + { + account:"cxKLTnnbsjsWQen47Sb73WfvCSsWAQfjNd6hRW86EqKC9Frhu" + simpleParams: { + tokenId: {integer:1} + amount:2 + } + }, + { + account:"cxM1YqYERiNvuEKkZUiUFefCtTfdy2q4Bj7QuWDfSPJuiuDWy" + simpleParams: { + tokenId: {integer:4} + amount:1 + } + } + ] + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "BatchTransfer": { + "id": 13974, + "transactionId": null, + "transactionHash": null, + "method": "BatchTransfer", + "state": "PENDING", + "encodedData": "0x280cc96f041013860712045df3393a7902eba3621115fdb7a6a4837349f7bc2fb676d57c000015660400", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## SimpleTransferToken + +The `SimpleTransferToken` mutation simplifies the process of transferring a specific token from one wallet to another. It is a straightforward way to facilitate token transfers without the need for complex intermediary steps. + + + +```graphql +mutation SimpleTransferToken { + SimpleTransferToken( + collectionId: "7154" + recipient: "0xc4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3" + params: { + tokenId: {integer:70468841277235617716769448539773927607} + amount:1 + } + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "SimpleTransferToken": { + "id": 14074, + "transactionId": null, + "transactionHash": null, + "method": "SimpleTransferToken", + "state": "PENDING", + "encodedData": "0x280600c4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3c96f0033b7007b94fad4fa2fe1d6a5b71ece03350400", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## ApproveToken + +The `ApproveToken` mutation is a powerful tool for establishing fine-grained permission systems at the individual token level within a specific token collection. It enables a token owner to grant another account (the operator) the authority to transfer a defined number of tokens from their ownership to other accounts. This level of control is particularly valuable when managing tokens with varying values, attributes, or use cases within a collection. + + + +```graphql +mutation ApproveToken { + ApproveToken( + collectionId: "7154" + tokenId: {integer:6533} + operator: "0x965bcdbb46614cbd79869e2eb568825f6c038cbdf9085edb1b164607d3738fa6" + amount: 1 + currentAmount: 1 + expiration: 445300 + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "ApproveToken": { + "id": 13855, + "transactionId": null, + "transactionHash": null, + "method": "ApproveToken", + "state": "PENDING", + "encodedData": "0x2811c96f1566965bcdbb46614cbd79869e2eb568825f6c038cbdf9085edb1b164607d3738fa6040174cb060004", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## UnapproveToken + +The `UnapproveToken` mutation serves the purpose of revoking approval for a specific account (referred to as the "operator") to transfer a particular token from a token account. Unlike `UnapproveCollection`, which revokes permissions over an entire collection, `UnapproveToken` provides fine-grained control by targeting individual tokens within a collection. This level of precision is valuable when managing tokens with distinct values, attributes, or permissions within a collection. + + + +```graphql +mutation UnapproveToken{ + UnapproveToken( + collectionId: 7154 + tokenId: {integer:70468841277235617716769448539773927607} + operator: "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "UnapproveToken": { + "id": 14079, + "transactionId": null, + "transactionHash": null, + "method": "UnapproveToken", + "state": "PENDING", + "encodedData": "0x2812c96f33b7007b94fad4fa2fe1d6a5b71ece0335d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## Freeze + +The `Freeze` mutation is a powerful tool that enables the temporary suspension of token transfers on a collection, token, collection account, or token account. This freezing action effectively prevents any further transfers from occurring, which also results in marketplace listings being unpurchaseable. The primary purpose of freezing is to provide administrative control, enhance security, investigate suspicious activities, or respond to unforeseen events that require the temporary halt of operations. + + + +```graphql +mutation Freeze { + Freeze( + freezeType: COLLECTION + collectionId: "10943" + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "Freeze": { + "id": 14064, + "transactionId": null, + "transactionHash": null, + "method": "Freeze", + "state": "PENDING", + "encodedData": "0x2807fdaa00", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## Thaw + +The `Thaw` mutation is a vital operation that allows you to unfreeze a previously frozen collection or token. Freezing temporarily prevents any transfers from occurring. Thawing reverses this action, restoring the collection or token's ability to be traded and transferred. + + + +```graphql +mutation Thaw{ + Thaw( + freezeType: TOKEN + collectionId: 7154 + tokenId: {integer:70468841277235617716769448539773927607} + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "Thaw": { + "id": 14075, + "transactionId": null, + "transactionHash": null, + "method": "Thaw", + "state": "PENDING", + "encodedData": "0x2808c96f01b7007b94fad4fa2fe1d6a5b71ece03350101", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## MutateToken + +The `MutateToken` mutation is a powerful tool for modifying specific properties of an individual token within a collection. It provides fine-grained control over token attributes, allowing you to make precise changes to a token's characteristics. In the provided example, the mutation is used to set the `listingForbidden` attribute to `true`, which effectively prohibits the token from being listed for sale or trade. + + + +```graphql +mutation MutateToken { + MutateToken( + collectionId: "7154" + tokenId: {integer:6533} + mutation: { + listingForbidden:true + } + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "MutateToken": { + "id": 14067, + "transactionId": null, + "transactionHash": null, + "method": "MutateToken", + "state": "PENDING", + "encodedData": "0x2803c96f15660100010100", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## OperatorTransferToken + +The `OperatorTransferToken` mutation is designed for operators to transfer tokens on behalf of the token owner. It enables the transfer of a specified amount of tokens from a source address to a recipient address, with the operation performed by an authorized operator. + + + +```graphql +mutation OperatorTransferToken{ + OperatorTransferToken( + collectionId: "7154" + recipient: "0x50a1ba0a184c9aca3a2ac7d427e96a676fe988454b4b56a62dd6622e843e890d" + params: { + tokenId: {integer:6533} + amount:1 + source:"0x1013860712045df3393a7902eba3621115fdb7a6a4837349f7bc2fb676d57c00" + } + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "OperatorTransferToken": { + "id": 14068, + "transactionId": null, + "transactionHash": null, + "method": "OperatorTransferToken", + "state": "PENDING", + "encodedData": "0x28060050a1ba0a184c9aca3a2ac7d427e96a676fe988454b4b56a62dd6622e843e890dc96f0115661013860712045df3393a7902eba3621115fdb7a6a4837349f7bc2fb676d57c000400", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## RemoveAllAttributes + +Remove all attributes from a collection and token. + + + +```graphql +mutation RemoveAllAttributes { + RemoveAllAttributes( + collectionId: "7154" + tokenId: {integer:6533} + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "RemoveAllAttributes": { + "id": 14069, + "transactionId": null, + "transactionHash": null, + "method": "RemoveAllAttributes", + "state": "PENDING", + "encodedData": "0x280bc96f018519000000000000000000000000000001000000", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## RemoveTokenAttribute + +The `RemoveTokenAttribute` mutation provides the capability to remove a specific attribute from an individual token within a collection. This operation is distinct from the `RemoveCollectionAttribute` mutation, which removes an attribute from all tokens within a collection. It is a precise way to manage token metadata, allowing for the removal of attributes that are no longer needed or contain errors. + + + +```graphql +mutation RemoveTokenAttribute { + RemoveTokenAttribute( + collectionId: "4741" + tokenId: {integer:0} + key: "name" + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "RemoveTokenAttribute": { + "id": 14071, + "transactionId": null, + "transactionHash": null, + "method": "RemoveTokenAttribute", + "state": "PENDING", + "encodedData": "0x280a154a0100000000000000000000000000000000106e616d65", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## TransferBalance + +The `TransferBalance` mutation is an operation designed for initiating the transfer of a specified amount of funds or balance from one account to another. It plays a fundamental role in enabling financial transactions by allowing users to send precise amounts of cryptocurrency or digital assets securely and transparently. + + + +```graphql +mutation TransferBalance { + TransferBalance( + recipient: "0xc4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3" + amount: 1 + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "TransferBalance": { + "id": 14077, + "transactionId": null, + "transactionHash": null, + "method": "TransferBalance", + "state": "PENDING", + "encodedData": "0x0a0300c4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a304", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## TransferAllBalance + +The `TransferAllBalance` mutation is designed for the purpose of transferring the entire balance from one account to another. It simplifies the process of moving all available funds from a sender's account to a recipient's account. This mutation is particularly useful for scenarios where you need to empty an account, consolidate funds from multiple accounts into one, or transfer ownership of an account by moving all assets to the new owner. + + + +```graphql +mutation TransferAllBalance { + TransferAllBalance( + recipient: "0xc4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3" + ) { + id + transactionId + transactionHash + method + state + encodedData + wallet { + account { + publicKey + address + } + } + } +} +``` + + +```json +{ + "data": { + "TransferAllBalance": { + "id": 14076, + "transactionId": null, + "transactionHash": null, + "method": "TransferAllBalance", + "state": "PENDING", + "encodedData": "0x0a0400c4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a301", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + } + } + } +} +``` + + + +## Infuse + +The Infuse mutation allows you to embed Enjin Coin (ENJ) into an existing token. This operation is crucial for enhancing the intrinsic value of digital assets, ensuring that they carry a guaranteed ENJ backing. By infusing ENJ, you provide tangible value to the token, which can only be retrieved through the process of "ing" the token. + +:::warning Calculating the infusion amount +The Platform accepts infusion values in the **base unit** (integers), not decimal ENJ amounts. To calculate the correct input, **multiply your desired ENJ amount by 10^18** (1 quintillion). + - **Formula:** `Desired ENJ infusion` \* `1,000,000,000,000,000,000` + - **Example:** To infuse a token with **5 ENJ**, input `5000000000000000000`. +::: + + + +```graphql +mutation Infuse{ + Infuse( + collectionId: 3298 #Specify the collection ID + tokenId: {integer: 1} #Specify the token ID + amount: 5000000000000000000 #Specify the amount of ENJ to infuse + ){ + id + method + state + } +} +``` + + +```json +{ + "data": { + "Infuse": { + "id": 386279, + "method": "Infuse", + "state": "PENDING" + } + } +} +``` + + diff --git a/versioned_docs/version-2.x/03-api-reference/02-mutations/04-wallets-mutations.md b/versioned_docs/version-2.x/03-api-reference/02-mutations/04-wallets-mutations.md new file mode 100644 index 0000000..7af0d0f --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/02-mutations/04-wallets-mutations.md @@ -0,0 +1,125 @@ +--- +title: "Wallets" +slug: "wallets" +description: "Perform wallets mutations in the Enjin API to manage wallet data, transfer assets, and update balances securely on the blockchain." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip Core Endpoints +- **Testnet:** `http://platform.canary.enjin.io/graphql` +- **Mainnet:** `http://platform.enjin.io/graphql` +::: + +This is a detailed reference guide that explains the most commonly used operations. + +## CreateWallet + +The `CreateWallet` mutation in is used to create a new wallet on the blockchain using an external identifier (external ID). This allows you to associate a wallet with an entity on the Enjin Platform. + + + +```graphql +mutation CreateWallet { + CreateWallet( + externalId: "e73f9f38-6832-4822-922b-b9225245ba24" + ) +} +``` + + +```json +{ + "data": { + "CreateWallet": true + } +} +``` + + + +## SetWalletAccount + +The `SetWalletAccount` mutation is used to associate an existing wallet account on the Enjin Platform with a game developer's external ID. This action links the user's wallet on the Enjin Platform to an external ID stored in the game developer's database, allowing the identification of a player's wallet within the Enjin Platform. + + + +```graphql +mutation SetWalletAccount{ + SetWalletAccount( + externalId: "ZMMP0FCq6NEKphFlNfESdbC81BXWebNHUiguJvFsb5dVnthIvBbFAaNfCE3mOJREDPS5lwWN4sDQXDV61fqRWUKVSeq4ItavScDS" + account: "cxLc8HSeuiLiYmEG7XB8wQrbkRPNCaDi5dVpqPkyhWdEp6i63" + ) +} +``` + + +```json +{ + "data": { + "SetWalletAccount": true + } +} +``` + + + +## VerifyAccount + +The `VerifyAccount` mutation is used by a wallet to confirm the ownership of a user account. This security measure ensures that actions performed on the account are authorized by the legitimate owner, enhancing the overall security and trustworthiness of the system. + + + +```graphql +mutation VerifyAccount{ + VerifyAccount( + verificationId: "0xfdb976d1c5fc9ed6d546f31332e756ca9c9ef6e3c6f0d9ea0ed0713b1cd84440" + signature: "0x8958f002f9513256fa329fa92441d39adc59e3a19165aeeb343acd72b7f6a" + account: "0xbeac4e574b46a3722da9239f97359b0440c0fb66e3b130f20c2284fd033f9138" + + ) +} +``` + + +```json +{ + "data": { + "VerifyAccount": true + } +} +``` + + + +## UpdateWalletExternalId + +The `UpdateWalletExternalId` mutation is used to update the external ID associated with a wallet account on the Enjin Platform. This action allows you to change the reference or identifier linked to a user's wallet within the Enjin Platform. + + + +```graphql +mutation UpdateWalletExternalId{ + UpdateWalletExternalId( + id: 8466 + newExternalId: "35b409dc-4f3f-41da-8d94-7635ef2a5853" + ) +} +``` + + +```json +{ + "data": { + "UpdateWalletExternalId": true + } +} +``` + + \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/02-mutations/05-fuel-tanks-mutations.md b/versioned_docs/version-2.x/03-api-reference/02-mutations/05-fuel-tanks-mutations.md new file mode 100644 index 0000000..a5d9ef3 --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/02-mutations/05-fuel-tanks-mutations.md @@ -0,0 +1,827 @@ +--- +title: "Fuel Tanks" +slug: "fuel-tanks" +description: "Use the Enjin API to perform fuel tanks mutations, including setting up and managing transaction cost coverage for users on the blockchain." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip Fuel Tanks Endpoints +- **Testnet:** `http://platform.canary.enjin.io/graphql/fuel-tanks` +- **Mainnet:** `http://platform.enjin.io/graphql/fuel-tanks` +::: + +This is a detailed reference guide that explains the most commonly used operations. + +## CreateFuelTank + +The `CreateFuelTank` mutation is used to create a new fuel tank within a system, allowing you to establish specific attributes and rules that govern how accounts can be added and how transactions are dispatched from it. This mutation is a fundamental step in setting up the infrastructure for managing accounts and transactions within a network, providing a customizable framework to tailor the tank's behavior to your specific requirements. + + + +```graphql +mutation CreateFuelTank { + CreateFuelTank( + name: "My Fuel Tank" + coveragePolicy: FEES_AND_DEPOSIT + reservesAccountCreationDeposit: false + requireAccount: true + accountRules: {requireToken: {collectionId: "7406", tokenId: {integer: 5839}}, whitelistedCallers: ["0x66f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f646009"]} + dispatchRules: [{requireToken: {collectionId: "7406", tokenId: {integer: 5839}}, whitelistedCallers: ["0x66f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f646009"], whitelistedCollections: null, maxFuelBurnPerTransaction: 0, userFuelBudget: {amount: 1, resetPeriod: 1}, tankFuelBudget: {amount: 1, resetPeriod: 1}}] + ) { + id + transactionId + transactionHash + state + encodedData + method + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} +``` + + +```json +{ + "data": { + "CreateFuelTank": { + "id": 379503, + "transactionId": null, + "transactionHash": null, + "state": "PENDING", + "encodedData": "0x3600404d79204675656c2054616e6b203132330100040000000014000466f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f64600905ee1c0000000000000000000000000000cf1600000000000000000000000000000200000000000000000000000000000000030401000000040401000000010108000466f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f64600901ee1c0000000000000000000000000000cf160000000000000000000000000000", + "method": "CreateFuelTank", + "wallet": null, + "idempotencyKey": "ba66208f-d565-4605-bc6b-58756f88814e" + } + } +} +``` + + + +## DestroyFuelTank + +The `DestroyFuelTank` mutation is used to permanently remove an existing fuel tank from the system. This operation is essential for efficient resource management, allowing you to clean up unused or unnecessary fuel tanks within your network infrastructure. + + + +```graphql +mutation DestroyFuelTank { + DestroyFuelTank(tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr") { + id + transactionId + transactionHash + state + encodedData + method + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} +``` + + +```json +{ + "data": { + "DestroyFuelTank": { + "id": 11280, + "transactionId": null, + "transactionHash": null, + "state": "PENDING", + "encodedData": "0x360d009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d", + "method": "DestroyFuelTank", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "idempotencyKey": "4c0440c5-340f-4de6-a3df-49f464f46656" + } + } +} +``` + + + +## AddAccount + +The `AddAccount` mutation is used to add a new account to an existing fuel tank. This operation is crucial when you need to grant access to specific users, allowing them to utilize the resources and perform authorized actions within the fuel tank. + + + +```graphql +mutation AddAccount { + AddAccount( + tankId: "cxNkbpQpiiza3JCH9fxmt6eJ8vDSFqzqVfz2a543QbPG4sN9K" + userId: "cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts" + ) { + id + transactionId + transactionHash + state + encodedData + method + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} +``` + + +```json +{ + "data": { + "AddAccount": { + "id": 11249, + "transactionId": null, + "transactionHash": null, + "state": "PENDING", + "encodedData": "0x360200bf71212e94365d5d3c5e7b2700a3bb9e2a19016b6502f625162024eb87fdcf7c00985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050", + "method": "AddAccount", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "idempotencyKey": "da44d553-6a39-460b-9dd8-b35c6984016c" + } + } +} +``` + + + +## BatchAddAccount + +The `BatchAddAccount` mutation is designed for efficiently adding multiple new user accounts to a specified fuel tank in a single batch operation. This feature is particularly useful when you need to add several accounts at once, reducing the number of individual requests and optimizing resource usage. + + + +```graphql +mutation BatchAddAccount { + BatchAddAccount(tankId: "cxNkbpQpiiza3JCH9fxmt6eJ8vDSFqzqVfz2a543QbPG4sN9K", userIds: ["cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts","cxLc8HSeuiLiYmEG7XB8wQrbkRPNCaDi5dVpqPkyhWdEp6i63"]) { + id + transactionId + transactionHash + state + encodedData + method + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} + +``` + + +```json +{ + "data": { + "BatchAddAccount": { + "id": 11256, + "transactionId": null, + "transactionHash": null, + "state": "PENDING", + "encodedData": "0x360a00bf71212e94365d5d3c5e7b2700a3bb9e2a19016b6502f625162024eb87fdcf7c0800985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d0500060820310ea5b09bef944c50a7f3ae82166304dd637263b1ba7e0fa0bab1f3f7b", + "method": "BatchAddAccount", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "idempotencyKey": "76f65815-8743-4ab9-8e01-b3fc62137337" + } + } +} +``` + + + +## BatchRemoveAccount + +The `BatchRemoveAccount` mutation is designed for efficiently removing multiple user accounts from a specified fuel tank in a single batch operation. This feature is particularly useful when you need to remove several accounts at once, reducing the number of individual removal requests and optimizing resource usage. + + + +```graphql +mutation BatchRemoveAccount { + BatchRemoveAccount(tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr", userIds: ["cxLkamf2QxHK9bcqhh56QAFc8TTmvWG33sUbkNakJV4yzdDcz"]) { + id + transactionId + transactionHash + state + encodedData + method + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} +``` + + +```json +{ + "data": { + "BatchRemoveAccount": { + "id": 11279, + "transactionId": null, + "transactionHash": null, + "state": "PENDING", + "encodedData": "0x360b009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d040066f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f646009", + "method": "BatchRemoveAccount", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "idempotencyKey": "af81b86e-2a71-4cf7-8784-60b777fed366" + } + } +} +``` + + + +## Dispatch + +The `Dispatch` mutation is a powerful operation that allows you to trigger specific actions within a system using a designated fuel tank (`tankId`) while adhering to predefined rules specified by a rule set (`ruleSetId`). This mutation is especially useful when you want the fuel tank to handle transaction fees and potentially storage deposits for the dispatched call. + + + +```graphql +mutation Dispatch { + Dispatch( + tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr" + ruleSetId: 0 + dispatch: { + call:FUEL_TANKS + query:"mutation { AddAccount(tankId: \"cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr\", userId: \"cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts\") { id encodedData } }" + variables:null + } + paysRemainingFee: false + ) { + id + transactionId + transactionHash + state + encodedData + method + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} +``` + + +```json +{ + "data": { + "Dispatch": { + "id": 11286, + "transactionId": null, + "transactionHash": null, + "state": "PENDING", + "encodedData": "0x3605009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d000000003602009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d00985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d05000", + "method": "Dispatch", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "idempotencyKey": "850956b8-b9e9-4e2d-98f4-ae7775e2d066" + } + } +} +``` + + + +## DispatchAndTouch + +The `DispatchAndTouch` mutation combines the functionalities of the `Dispatch` mutation with the ability to create an account for the calling entity (`origin`) if it doesn't already exist. This mutation is especially valuable in scenarios where the caller's account may not have been set up in advance, streamlining the process of both executing an operation and ensuring account existence. + + + +```graphql +mutation DispatchAndTouch { + DispatchAndTouch( + tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr" + ruleSetId: 0 + dispatch: { + call:FUEL_TANKS + query:"mutation { AddAccount(tankId: \"cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr\", userId: \"cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts\") { id encodedData } }" + variables:null + } + paysRemainingFee: false + ) { + id + transactionId + transactionHash + state + encodedData + method + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} +``` + + +```json +{ + "data": { + "DispatchAndTouch": { + "id": 11284, + "transactionId": null, + "transactionHash": null, + "state": "PENDING", + "encodedData": "0x3606009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d000000003602009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d00985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d05000", + "method": "DispatchAndTouch", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "idempotencyKey": "a9e62906-ac2f-4454-b17e-c0e5bf774da0" + } + } +} +``` + + + +## SetConsumption + +The `SetConsumption` mutation enables you to manually set the fuel consumption for a specific fuel tank. It allows you to override the current fuel consumption data with the values provided in the mutation arguments. This can be useful for making adjustments, corrections, or administrative changes to fuel consumption records. + + + +```graphql +mutation SetConsumption { + SetConsumption( + tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr" + ruleSetId: 0 + userId: "cxLkamf2QxHK9bcqhh56QAFc8TTmvWG33sUbkNakJV4yzdDcz" + totalConsumed: 10 + lastResetBlock: 100 + ) { + id + transactionId + transactionHash + state + encodedData + method + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} +``` + + +```json +{ + "data": { + "SetConsumption": { + "id": 11287, + "transactionId": null, + "transactionHash": null, + "state": "PENDING", + "encodedData": "0x360c009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d010066f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f64600900000000280164000000", + "method": "SetConsumption", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "idempotencyKey": "9bc5fc3d-d5d2-415c-a0bf-96e5de529502" + } + } +} +``` + + + +## InsertRuleSet + +The `InsertRuleSet` mutation is used to add a new rule set to a fuel tank. It allows you to define specific rules and requirements for the fuel tank's operation. + + + +```graphql +mutation InsertRuleSet { + InsertRuleSet( + tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr" + ruleSetId: 1 + dispatchRules: { requireToken: {collectionId: "7153", tokenId: {integer: 68}} } + ) { + id + transactionId + transactionHash + state + encodedData + method + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} +``` + + +```json +{ + "data": { + "InsertRuleSet": { + "id": 11288, + "transactionId": null, + "transactionHash": null, + "state": "PENDING", + "encodedData": "0x3608009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d0100000000", + "method": "InsertRuleSet", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "idempotencyKey": "2ed39eca-dc04-408d-ac99-1b44c19e2e90" + } + } +} +``` + + + +### Rules + +When a call is made to a fuel tank, it must be made in accordance with a set of rules. These rules, known as rule sets, can include multiple individual rules that determine the validity of the call. A fuel tank can have multiple rule sets, each of which controls access and permissions to the fuel tank's functionality and resources. + +##### Whitelisted Callers + +Whitelisted callers are accounts that are explicitly granted permission to make calls to a fuel tank. Any calls made by accounts not on the whitelist will be rejected and fail. + +##### Whitelisted Collections + +Whitelisted collections refer to a list of specific collections allowed to be called by dispatch on a fuel tank. If a dispatch call is made to any collection, not on the whitelist, it will be rejected and fail. This ensures that calls are only made to the specific collections authorized to be accessed. + +##### Max Fuel Burn Per Transaction + +It is a setting that limits the amount of fuel that can be consumed in a single transaction. It is a safety measure that helps prevent misuse and overuse of the fuel tank's resources by ensuring that a single transaction doesn't consume too much fuel, which is important to ensure a sustainable network. + +##### User Fuel Budget + +A user fuel budget is the total amount of fuel allocated to a specific user across all rule sets of a fuel tank. It sets a limit on the amount of fuel that can be consumed by a user's transactions. Once the user's fuel budget is exhausted, their transactions will fail until more fuel is added to their budget or their overall consumption is lowered , which is meant to avoid the overuse of resources and ensure a sustainable network.\*\* + +##### Tank Fuel Budget + +A tank fuel budget is the total amount of fuel allocated to a specific fuel tank across all its rule sets. It sets a limit on the amount of fuel that can be consumed by all transactions made through that tank. Once the tank's fuel budget is exhausted, all transactions will fail until more fuel is added to the tank's budget or overall consumption is lowered. + +##### Require Token + +The "Require token" setting indicates that a specific token must be held by the caller for their call to be accepted by the fuel tank. If the caller does not possess the required token, their call will be rejected and fail. This feature is intended to ensure that only users who hold the specified token can access the fuel tank's functions and resources. + +##### Permitted Calls + +Permitted calls refer to a list of specific function calls that are allowed to be made using this rule set. Any calls made to other functions will be rejected and fail. This ensures that only authorized calls can be made by the users, and also, by setting a boundary to what calls are allowed and what calls are not, it helps to protect the network from unwanted transactions that could impact negatively on the network. + +##### Permitted Extrinsics + +Permitted extrinsics refer to a list of specific extrinsics that are allowed to be made using this rule set. Any extrinsics that are not on the list will be rejected and fail. This feature is used to restrict access to specific functionality and resources on the network and ensure that only authorized calls can be made by users. When setting this rule, it's important to note that the parameters passed to the extrinsic do not affect the outcome, only the function name is considered.\*\* + +##### Freezing + +"Freezing" is a state where a fuel tank or a rule set is temporarily prevented from accepting calls or making changes. This means that while a fuel tank or rule set is frozen, no dispatches are allowed to occur on it. This is implemented as a safety measure to prevent accidental or malicious changes and to ensure that the fuel tank or rule set remains in a stable state until the freeze is lifted. + +##### Descriptor + +A descriptor is a list of all the data needed to create a fuel tank. It includes details such as the fuel tank's name, account management rules, rule sets and other configuration information related to the fuel tank. The descriptor acts as a blueprint that defines how the fuel tank is set up and how it will function. It contains all the information needed to create and configure the fuel tank, and can be used to modify the fuel tank's settings. + +## MutateFuelTank + +The `MutateFuelTank` mutation is used to apply a specific mutation to a fuel tank. This mutation allows you to modify the Fuel Tank configuration. + + + +```graphql +mutation MutateFuelTank { + MutateFuelTank(tankId: "cxLt3kZZHgz4pqGgaQWjxCRXGd81ey7YRwycGcRnZrNtfpbAs", + mutation: { + coveragePolicy: FEES_AND_DEPOSIT, + reservesAccountCreationDeposit: true + accountRules: { + whitelistedCallers: [ + "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", + "cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP" + ] + } + } + ){ + id + transactionId + transactionHash + state + encodedData + method + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} +``` + + +```json +{ + "data": { + "MutateFuelTank": { + "id": 386329, + "transactionId": null, + "transactionHash": null, + "state": "PENDING", + "encodedData": "0x3601006ca6ae51167284d08d113d0d04dd502e7c827fff54d07c9215f601c202f335560101010101010400082c42a1565cf51b8c6ed5ac06ebb7c1c5999ff0076879fd1ba746c9b5d5029d3644479350952f776be51e7b79010a71203cb18d805fbaaf64990ae40790007525", + "method": "MutateFuelTank", + "wallet": null, + "idempotencyKey": "9ffdd07e-a04a-4116-affa-b73ad3c8f4d8" + } + } +} +``` + + + +## RemoveAccount + +The `RemoveAccount` mutation is used to remove a specified user account from a fuel tank. This mutation is particularly useful for managing the accounts associated with a fuel tank. + + + +```graphql +mutation RemoveAccount { + RemoveAccount(tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr", userId: "cxLkamf2QxHK9bcqhh56QAFc8TTmvWG33sUbkNakJV4yzdDcz") { + id + transactionId + transactionHash + state + encodedData + method + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} +``` + + +```json +{ + "data": { + "RemoveAccount": { + "id": 11291, + "transactionId": null, + "transactionHash": null, + "state": "PENDING", + "encodedData": "0x3603009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d0066f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f646009", + "method": "RemoveAccount", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "idempotencyKey": "c63c8c9d-3fcb-412a-afac-703da5c2b145" + } + } +} +``` + + + +## RemoveAccountRuleData + +The `RemoveAccountRuleData` mutation is used to delete specific rule data associated with an account on a fuel tank. This allows you to remove certain restrictions or requirements that were previously set for an account's interactions with the fuel tank. + + + +```graphql +mutation RemoveAccountRuleData { + RemoveAccountRuleData( + tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr" + ruleSetId: 0 + userId: "cxLkamf2QxHK9bcqhh56QAFc8TTmvWG33sUbkNakJV4yzdDcz" + rule: REQUIRE_TOKEN + ) { + id + transactionId + transactionHash + state + encodedData + method + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} +``` + + +```json +{ + "data": { + "RemoveAccountRuleData": { + "id": 11292, + "transactionId": null, + "transactionHash": null, + "state": "PENDING", + "encodedData": "0x3604009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d0066f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f6460090000000005", + "method": "RemoveAccountRuleData", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "idempotencyKey": "d3e79021-c7a4-4a28-a29d-effd90d95933" + } + } +} +``` + + + +## RemoveRuleSet + +The `RemoveRuleSet` mutation is utilized to completely remove an existing set of rules from a specified fuel tank. Rule sets represent collections of conditions or constraints that govern the usage and access to the fuel tank. Removing a rule set becomes necessary when you need to modify operational policies, or a rule set is no longer applicable to the fuel tank's operations. + + + +```graphql +mutation RemoveRuleSet { + RemoveRuleSet(tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr", ruleSetId: 0) { + id + transactionId + transactionHash + state + encodedData + method + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} +``` + + +```json +{ + "data": { + "RemoveRuleSet": { + "id": 11293, + "transactionId": null, + "transactionHash": null, + "state": "PENDING", + "encodedData": "0x3609009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d00000000", + "method": "RemoveRuleSet", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "idempotencyKey": "6f05d6c2-69ee-42ca-8a5c-f55c8a34da8b" + } + } +} +``` + + + +## ScheduleMutateFreezeState + +The `ScheduleMutateFreezeState` mutation enables you to schedule a change in the `is_frozen` state of either a fuel tank or a rule set. This state determines whether the tank or rule set can be frozen (immobilized), preventing any operations that could alter its configuration or state. + + + +```graphql +mutation ScheduleMutateFreezeState { + ScheduleMutateFreezeState( + tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr" + ruleSetId: 0 + isFrozen: true + ) { + id + transactionId + transactionHash + state + encodedData + method + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} +``` + + +```json +{ + "data": { + "ScheduleMutateFreezeState": { + "id": 11294, + "transactionId": null, + "transactionHash": null, + "state": "PENDING", + "encodedData": "0x3607009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d010000000001", + "method": "ScheduleMutateFreezeState", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "idempotencyKey": "e2609c5a-7b0d-4f1a-88c8-cf92c9491549" + } + } +} +``` + + \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/02-mutations/06-marketplace-mutations.md b/versioned_docs/version-2.x/03-api-reference/02-mutations/06-marketplace-mutations.md new file mode 100644 index 0000000..7105d7b --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/02-mutations/06-marketplace-mutations.md @@ -0,0 +1,349 @@ +--- +title: "Marketplace" +slug: "marketplace" +description: "Use the Enjin API to perform marketplace mutations, including creating and modifying token listings, managing trades, and marketplace transactions." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip Marketplace Endpoints +- **Testnet:** `http://platform.canary.enjin.io/graphql/marketplace` +- **Mainnet:** `http://platform.enjin.io/graphql/marketplace` +::: + +This is a detailed reference guide that explains the most commonly used operations. + +## CreateListing + +The `CreateListing` mutation is used to initiate the process of placing a buy or a sell order on a marketplace. This mutation is essential for sellers who want to list their assets for sale in the marketplace, or buyers who wants to purchase a token. Buy orders are Offers, and sell orders are either Fixed Listings or Auction Listings. + +### Use Case: + +The `CreateListing` mutation is a fundamental operation in a marketplace. It allows you, as a seller, to list your assets for sale on the platform, and as a buyer, to create a buy offer. Here's how you can use it: + +1. **Asset Details**: Specify the asset you want to buy/sell and the asset you want to receive in exchange. + When creating a sell order, specify the asset you want to sell in the `makeAssetId` parameter, and the asset you want to receive in exchange in the `takeAssetId` parameter. + When creating a buy order, specify the asset you want to buy in the `takeAssetId` parameter, and the asset you want to pay with in the `makeAssetId` parameter. + +:::info +For ENJ tokens, use `collectionId: 0` and `tokenId: {integer: 0}` +::: + +2. **Quantity and Price**: Set the quantity (`amount`) and the price (`price`) at which you are willing to buy/sell the asset. +3. **Uniqueness**: Include a random string (`salt`) to ensure the uniqueness of your listing order. +4. **start/end block (Optional)**: If you are conducting an auction, you can specify the start and end blocks for the auction (`auctionParams`). + If you are conducting an offer, you can specify the end block for the auction (`offerParams`). +5. **Response**: After successfully creating the listing, you will receive a response with details about the new listing, including its ID, state, and other relevant information. + +This mutation is essential for both buyers and sellers to participate in the marketplace, list their assets and offers, and potentially initiate sales or auctions. It forms the basis for trading assets within the platform. + +### Create Fixed Listing + + + +```graphql +mutation CreateOffer{ + CreateListing( + makeAssetId: { + collectionId: 91829 + tokenId: {integer: 1} + } + takeAssetId: { + collectionId: 0 + tokenId: {integer: 0} + } + amount: 1 + price: 10000000000000000000 + salt: "random string" + listingData: { + type: FIXED_PRICE + } + ){ + id + method + state + } +} +``` + + +```json +{ + "data": { + "CreateListing": { + "id": 386427, + "method": "CreateListing", + "state": "PENDING" + } + } +} +``` + + + +### Create Auction Listing + + + +```graphql +mutation CreateOffer{ + CreateListing( + makeAssetId: { + collectionId: 91829 + tokenId: {integer: 1} + } + takeAssetId: { + collectionId: 0 + tokenId: {integer: 0} + } + amount: 1 + price: 5000000000000000000 + salt: "random string" + listingData: { + type: AUCTION + auctionParams: { + startBlock: 3818000 + endBlock: 1000000000 + } + } + ){ + id + method + state + } +} +``` + + +```json +{ + "data": { + "CreateListing": { + "id": 386422, + "method": "CreateListing", + "state": "PENDING" + } + } +} +``` + + + +### Create Offer Listing + + + +```graphql +mutation CreateOffer{ + CreateListing( + makeAssetId: { + collectionId: 0 + tokenId: {integer: 0} + } + takeAssetId: { + collectionId: 91829 + tokenId: {integer: 1} + } + amount: 1 + price: 5000000000000000000 + salt: "random string" + listingData: { + type: OFFER + offerParams: { + expiration: 100000000 + } + } + ){ + id + method + state + } +} +``` + + +```json +{ + "data": { + "CreateListing": { + "id": 386394, + "method": "CreateListing", + "state": "PENDING" + } + } +} +``` + + + +## FillListing + +The `FillListing` mutation is used to finalize the purchase of an asset listed at a fixed price on a marketplace, as well as offers. It enables buyers to accept the terms of a listing and execute the transaction to acquire the asset they desire and enables sellers to accept an offer. + + + +```graphql +mutation FillListing { + FillListing(listingId: "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20", amount: 1) { + id + transactionId + transactionHash + state + encodedData + method + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} +``` + + +```json +{ + "data": { + "FillListing": { + "id": 11297, + "transactionId": null, + "transactionHash": null, + "state": "PENDING", + "encodedData": "0x3202101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f2004", + "method": "FillListing", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "idempotencyKey": "0f4eb316-cf7f-4561-a2d6-112abf08d323" + } + } +} +``` + + + +### Use Case: + +The `FillListing` mutation is a critical operation for buyers in a marketplace. Here's how you can use it: + +1. **Select Listing**: You specify the unique `listingId` of the listing you want to fill, indicating the asset you wish to purchase. +2. **Quantity**: Determine the `amount` of the asset you want to buy. Ensure that this quantity matches the available quantity in the listing if it's a fixed price sale. +3. **Transaction Submission**: Execute the mutation, triggering the transaction. The response will provide transaction details, including the initial state as "PENDING." +4. **Transaction Processing**: The network processes the transaction, and eventually, the `transactionId` and `transactionHash` fields in the response will be updated with the respective values once confirmed. +5. **Completion**: The successful execution of the `FillListing` mutation indicates that you have committed to the purchase, and the asset will be transferred to your wallet upon transaction confirmation. +6. **Accepting Offers**: The mutation is also used by seller to accept a buy offer and sell the asset to the buyer. + +This mutation is essential for buyers to participate in the marketplace, finalize purchases, and take ownership of assets. It facilitates the transfer of both digital assets and funds between parties on the blockchain, ensuring a seamless and secure transaction process. + +## FinalizeAuction + +The `FinalizeAuction` mutation is used to complete the auction process for a specific asset listed in a marketplace. This operation is crucial for officially closing the auction and transferring the asset to the highest bidder, typically after the auction has ended. + + + +```graphql +mutation FinalizeAuction { + FinalizeAuction(listingId: "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20") { + id + transactionId + transactionHash + state + encodedData + method + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} +``` + + +```json +{ + "data": { + "FinalizeAuction": { + "id": 11298, + "transactionId": null, + "transactionHash": null, + "state": "PENDING", + "encodedData": "0x3204101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20", + "method": "FinalizeAuction", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "idempotencyKey": "0deaaad8-f6a0-4ef5-b8f1-fadd1ecb8ac5" + } + } +} +``` + + + +## CancelListing + +The `CancelListing` mutation in used to cancel a listing on the Enjin Blockchain marketplace. This operation is typically performed by the seller or the system administrator and is used to remove an asset from a marketplace listing. + + + +```graphql +mutation CancelListing { + CancelListing(listingId: "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20") { + id + transactionId + transactionHash + state + encodedData + method + wallet { + account { + publicKey + address + } + } + idempotencyKey + } +} +``` + + +```json +{ + "data": { + "CancelListing": { + "id": 11295, + "transactionId": null, + "transactionHash": null, + "state": "PENDING", + "encodedData": "0x3201101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20", + "method": "CancelListing", + "wallet": { + "account": { + "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", + "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" + } + }, + "idempotencyKey": "bfdb8fe9-3d93-4322-bf16-077c9566050d" + } + } +} +``` + + \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/02-mutations/07-beam-mutations.md b/versioned_docs/version-2.x/03-api-reference/02-mutations/07-beam-mutations.md new file mode 100644 index 0000000..9b29227 --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/02-mutations/07-beam-mutations.md @@ -0,0 +1,130 @@ +--- +title: "Beam" +slug: "beam" +description: "Perform Beam mutations in the Enjin API to manage QR code activations, enabling blockchain asset distribution via QR code scans." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip Beam Endpoints +- **Testnet:** `http://platform.canary.enjin.io/graphql/beam` +- **Mainnet:** `http://platform.enjin.io/graphql/beam` +::: + +This is a detailed reference guide that explains the most commonly used operations. + +## CreateBeam + +The `CreateBeam` mutation is used to create a new Enjin Beam distribution campaign. Enjin Beam is a method for distributing tokens via QR codes. This mutation sets up the details and parameters of the Beam campaign. + + + +```graphql +mutation CreateBeam { + CreateBeam( + name: "My Beam" + description: "This is a test beam." + image: "https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg" + start: "20230101" + end: "20240101" + collectionId: 7153 + tokens: [ + {tokenIds:"1..10" tokenQuantityPerClaim:1 claimQuantity:1 type:MINT_ON_DEMAND} + ] + ) +} +``` + + +```json +{ + "data": { + "CreateBeam": "cd6443f822cf6e89c03edd4f6fbc6e55" + } +} +``` + + + +## ClaimBeam + +The `ClaimBeam` is used to claim a specific Enjin Beam using a unique code. Enjin Beam is a method for distributing tokens, and this mutation allows users to claim their allocated tokens by providing the necessary code and cryptographic signature. + + + +```graphql +mutation ClaimBeam { + ClaimBeam( + code: "cd6443f822cf6e89c03edd4f6fbc6e55" + account: "cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts" + signature:"0x1d670c35a76efa984fa2bd655d21eb28af0ee470e359848d2105845e170bb589fd6b7c2cbe97c9acce1eb205d4ade39cae75c619230cf44b64b25ec8677d7f0f" + cryptoSignatureType:SR25519 + ) +} +``` + + +```json +{ + "data": { + "ClaimBeam": true + } +} +``` + + + +## UpdateBeam + +The `UpdateBeam` mutation is used to modify the properties of an existing Enjin Beam. It allows you to update specific attributes of a Beam, such as its name, to ensure that the campaign remains accurate and adaptable. + + + +```graphql +mutation UpdateBeam { + UpdateBeam( + code: "ca8524349abab1cc20994841a8e0f175" + name: "My beam UPDATED" + ) +} +``` + + +```json +{ + "data": { + "UpdateBeam": true + } +} +``` + + + +## DeleteBeam + +The `DeleteBeam` mutation is used to delete an existing Enjin Beam from the system. It allows you to remove a Beam and all of its associated data based on its unique identifier code. + + + +```graphql +mutation DeleteBeam { + DeleteBeam(code: "cd6443f822cf6e89c03edd4f6fbc6e55") +} +``` + + +```json +{ + "data": { + "DeleteBeam": true + } +} +``` + + diff --git a/versioned_docs/version-2.x/03-api-reference/02-mutations/_category_.json b/versioned_docs/version-2.x/03-api-reference/02-mutations/_category_.json new file mode 100644 index 0000000..ef4a0fc --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/02-mutations/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Mutations (Write)" +} + \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/03-api-reference.md b/versioned_docs/version-2.x/03-api-reference/03-api-reference.md new file mode 100644 index 0000000..d2b9c57 --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/03-api-reference.md @@ -0,0 +1,93 @@ +--- +title: "API Reference" +slug: "/api-reference" +--- +The Enjin API provides a robust GraphQL interface for interacting with the Enjin Platform. This guide will demonstrate how to explore available queries and mutations using the GraphiQL Playground. + +## Exploring the GraphiQL Playground + +The GraphiQL Playground is an interactive environment where developers can explore the full range of API functionalities and build queries/mutations. Here’s how you can navigate and utilize this tool effectively. + +### Accessing the GraphiQL Playground + +You can access the GraphiQL Playground for both Testnet and Mainnet environments: + +- **Testnet**: [Core Operations](https://platform.canary.enjin.io/graphiql) +- **Mainnet**: [Core Operations](https://platform.enjin.io/graphiql) + +### Navigating the Documentation Explorer + +#### Step 1: Open the Documentation Explorer + +- Start by opening the GraphiQL Playground and click the `Show Documentation Explorer` button located at the top left. This will display a panel listing all available queries, mutations, and types. + +![Show Documentation Explorer](/img/getting-started/show-documentation-explorer.gif) + +#### Step 2: Search for a Specific Mutation + +- In the Documentation Explorer, you can use the search input to find a specific mutation. For example, to find the "CreateToken" mutation, type "Create" into the search bar. This helps narrow down the list to relevant operations. + +![Search mutations or queries](/img/getting-started/search.gif) + +:::tip Tip: quick Access to Documentation +If you already have a query or mutation in your builder, you can quickly open its documentation page by clicking on it while holding the `Ctrl` button. This will navigate you directly to its details in the Documentation Explorer. +::: + +#### Step 3: Explore the Mutation Details + +- Click on the "CreateToken" mutation to view detailed information about it. You will see: + - **Overview**: A description of what the mutation does. + - **Return Type**: The type of data it returns, such as a "transaction". + - **Arguments**: A list of arguments it accepts, with descriptions and data types. + +![Explore mutation, query, or type](/img/getting-started/explore-mutation-query-type.gif) + +### Understanding Arguments and Types + +#### Example: Creating a Token with Initial Supply of 10 + +- For instance, if you want to create a token with an initial supply of 10, you will need to focus on the `params` argument. This argument is of type "CreateTokenParams", a complex type. + +

+ Example of exploring an argument +

+ +- Click on `CreateTokenParams` to explore its fields: + - **tokenId**: Of type "EncodableTokenIdInput", which specifies the token identifier. + - **initialSupply**: Of type "BigInt", which you would set to 10. + - **cap**: Of type "TokenMintCap", which may define the maximum supply (optional). + +

+ Example of exploring a complex type argument +

+ +#### Constructing Your Mutation + +- Using the information from the Documentation Explorer, you can construct a mutation to create a token. Here is an example: + +```graphql +mutation { + CreateToken( + recipient: "efRecipientAddress" + collectionId: "12345" + params: { + tokenId: { integer: 1 } + initialSupply: 10 + } + ) { + id + method + state + } +} +``` + +- This mutation creates a new token with an initial supply of 10 in the specified collection. Adjust the parameters as needed for your application. + +:::tip Not sure how to construct the query/mutation? +Check out our [GraphQL Query Builder Guide](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#graphiql-playground) for detailed instructions on building queries and mutations. +::: + +#### Conclusion + +By using the Documentation Explorer in the GraphiQL Playground, you can efficiently navigate and understand the Enjin API's queries, mutations, and field types. diff --git a/versioned_docs/version-2.x/03-api-reference/03-websocket-events.md b/versioned_docs/version-2.x/03-api-reference/03-websocket-events.md new file mode 100644 index 0000000..f2325ac --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/03-websocket-events.md @@ -0,0 +1,41 @@ +--- +title: "Websocket Events" +slug: "websocket-events" +--- +:::info Websocket URLs +Enjin Platform Cloud **Canary**: `wss://ws-us2.pusher.com:443/app/76b7604dabc23c62be75?protocol=7`\ +Enjin Platform Cloud **Mainnet**: `wss://ws-us2.pusher.com:443/app/02cbd93e7842fb1db299?protocol=7` +::: + +The Enjin Platform emits events that you can listen to for various operations. +For example, whenever a transaction is issued by your wallet daemon, an event is emitted via Websocket. + +Listening to events is especially useful when awaiting for a transaction to be approved on-chain. + +Platform events are emitted to the platform Websocket endpoint, in various channels. +For example, we can listen to events related to account address `cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f` by subscribing to the channel `0x5a6aae294416f3e875d9a8975658905002cfd3e5e64105d76296c4b0adbfd77e`, which is the account's public key. + +:::note Note on Address Formats in Platform Events +Please note that all account addresses included in platform events, such as channel names or any addresses within the event payload, are formatted as **public keys**.\ +For example, in a "Transfer" event, the "from" account address appears in its public key format.\ +If needed, you may use the [Account Format Transform tool](https://matrix.subscan.io/tools/format_transform) to convert between **SS58-encoded address format** (e.g., "cx...123" for the Canary Matrixchain or "cn...123" for the Canary Relaychain) and public key format. +::: + +```json +{ + "event":"pusher:subscribe", + "data":{ + "channel":"0x5a6aae294416f3e875d9a8975658905002cfd3e5e64105d76296c4b0adbfd77e" + } +} +``` + +![Example banner](/img/api-functions/Pusher.gif) + +To get the full list of events and the channels they are broadcasted in, send the `GetEvents` query: + +```graphql +query { + GetEvents +} +``` diff --git a/versioned_docs/version-2.x/03-api-reference/04-important-arguments.md b/versioned_docs/version-2.x/03-api-reference/04-important-arguments.md new file mode 100644 index 0000000..d95b179 --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/04-important-arguments.md @@ -0,0 +1,434 @@ +--- +title: "Arguments" +slug: "important-arguments" +description: "A quick reference for some important arguments used in the Enjin API Schema." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +This is a detailed reference guide that explains most common arguments within the Enjin API Schema. + +## id + +The `id` argument is a unique identifier used to specifically identify a query or mutation that occurred on the Enjin Platform. + +It helps pinpoint a particular operation when you want to reference it or perform actions related to that operation. + +This identifier is typically generated by the Enjin API and serves as a reference point for interacting with specific operations. + +## transactionId + +A convenient way to rapidly reference blockchain transactions in a sequential order. + +It is structured in the format `xxxxxx-y`, where `xxxxxx` denotes the block number, and `y` indicates the position of the transaction within that block. + +## transactionHash + +The "transactionHash" argument in GraphQL serves as a unique identifier for a particular blockchain transaction. + +It is generated using a cryptographic hash function, guaranteeing that each "transactionHash" is entirely distinct and cannot resemble an identifier from a different blockchain network. + +## idempotencyKey + +A special key that prevents the same operation from being accidentally carried out more than once. It ensures that if you send the same request multiple times, it has the same effect as sending it once, avoiding unintended duplicates. + +## verificationId + +A unique identifier that is returned once you verify a user's ownership over a specific wallet. + +You will need to use the `verificationId` to find a wallet address once it's been verified but you can also use it to find other important information regarding the wallet. + +## after + +Used as a cursor to specify a starting point for fetching data in a paginated list. + +It helps you retrieve data after a specific item or position in the list. + +## first + +Used to specify the number of results or items you want to return per page when dealing with paginated data. + +Simply provide it with a numeric value that represents the desired number of results you want to retrieve in a single page. + +## account + +Your "Enjin Blockchain account" that acts like your digital identity on the blockchain. + +It allows you to interact with the blockchain, such as sending or receiving tokens. + +Think of it as your account on the blockchain network. + +### address + +A human-readable representation of your Enjin Blockchain account. + +Users typically interact with and see the address as their primary identifier in wallets and marketplaces. + +It's often in the form of a long alphanumeric string, making it easier for people to send you tokens. + +Your address is derived from your public key and serves as a more user-friendly way to identify your account. + +### publicKey + +A cryptographic key associated with your Enjin Blockchain account. + +It serves as your unique identifier on the blockchain. + +When someone wants to send you tokens or verify your transactions, they use your public key to do so. It's like your digital fingerprint. + +## hashes + +Allows you to filter blockchain transactions based on their unique transaction hashes. + +Transaction hashes are cryptographic representations of individual transactions on the blockchain, ensuring their immutability and uniqueness. + +## numbers + +Used for filtering transactions by block numbers, allows you to specify the block numbers from which you want to retrieve transactions. + +This is useful when you want to narrow down and retrieve transactions from specific blocks on the blockchain. + +## collectionId + +A defining identifier for token collections on the Enjin Blockchain. It is automatically generated by the blockchain when a new collection is created. + +Token collections play a pivotal role in organizing and managing related tokens, simplifying their accessibility and interaction within wallets and on marketplaces. + +## acknowledgeEvents + +Controls whether to automatically acknowledge events that were broadcast to the blockchain but have not yet been acknowledged by the blockchain itself. + +When events are acknowledged before finalization on the blockchain, the operation's `result` is set as `EXTRINSIC_FAILED`, indicating a failure in the blockchain's processing. + +## tokenId + +Used to request specific information about a particular token on the Enjin Blockchain. + +It allows you to retrieve details, properties, and attributes of the token identified by its unique token ID. + +### integer + +A straightforward numerical identifier for a token. It's a simple and efficient way to represent tokens. + +You might choose `integer` when you want a basic, numeric representation of tokens without additional complexity. + +It's useful for scenarios where simplicity and efficiency are more important than encoding additional information in the ID. + +### stringId + +Derived from a string, such as a name or label. It you to choose human-readable representations of tokens. + +You might choose `stringId` when you want to represent tokens with user-friendly names or labels. + +It's useful for mapping easily recognizable names. + +### hash + +A `hash` is created using a cryptographic hash function, ensuring uniqueness and collision resistance. + +It's suitable when uniqueness across collections is critical. + +It's often used in cases where tokens must be resistant to duplication or tampering. + +### ERC1155 + +Designed to facilitate seamless conversion between tokens Ethereum Virtual Machine (EVM) chains and tokens on Enjin Blockchain. + +Particularly useful for projects that require interoperability between blockchain ecosystems. + +It enables tokens to be converted from one chain to another while retaining their essential properties, making it a valuable tool for cross-chain token management and exchange. + +#### index + +A 16-bit integer that represents a specific instance or variation of a token type within an ERC-1155 contract. + +It is used in combination with the base token ID to uniquely identify individual tokens of the same type within the contract. + +#### tokenId + +A unique identifier used within the ERC-1155 token standard. + +It combines two components: a 16-bit hexadecimal base token ID and a 16-bit integer index. + +When these components are combined, they create a 128-bit integer that serves as a distinct identifier for a specific token within an ERC-1155 contract. + +## method + +Enables precise and efficient retrieval of transaction-specific method. + +This targeted approach offers a clear and detailed understanding of the exact actions performed within a transaction, simplifying debugging, security auditing, and user interaction. + +It streamlines data retrieval by focusing on the essential method details, making it a powerful tool for developers, auditors, analysts, and end-users to gain insights, enhance security, and improve the overall user experience in blockchain applications. + +## signedAtBlock + +Specifies the exact block at which a transaction was signed on the blockchain. + +It serves as a historical reference point, indicating precisely when the transaction was initiated and digitally signed within the blockchain's timeline. + +This information is particularly useful when there is a need find a transaction within a block, verify the authenticity of a digital signature, or ensure compliance with timestamp-related requirements. + +It enables users, developers, and auditors to track the temporal context of blockchain interactions, offering a clear historical record that can be crucial for auditing, legal, regulatory, and security purposes. + +## state + +Used for tracking and monitoring the progress of Enjin Platform queries and mutations, offering insights into their current status and history. Here are the various states and why they are useful: + +- **`ABANDONED`:** Indicates that the operation couldn't be sent, often due to encoding issues or insufficient funds. +- **`PENDING`:** Represents a newly created operation that is awaiting approval by a wallet. +- **`PROCESSING`:** Signifies that the Enjin Wallet Daemon has picked up the transaction and is actively processing it. +- **`BROADCAST`:** Indicates that the transaction has successfully sent the transaction to the Enjin Blockchain. +- **`FINALIZED`:** Denotes that the transaction has been successfully included in a block on the blockchain. Although, it doesn't specify whether the on-chain transaction succeeded or failed. That data can be queried by using the `result` field. + +## result + +Indicates whether a transaction has succeeded (`ExtrinsicSuccess`) or failed (`ExtrinsicFailed`). + +This is invaluable for transaction verification and error handling. + +It offers transparency and user confidence in blockchain interactions, aids in security audits and compliance checks, and enables automated processes to make decisions based on transaction outcomes. + +This single piece of information is pivotal for ensuring the reliability, integrity, and functionality of blockchain-based applications and systems, making it an essential component in blockchain transaction processing. + +## externalId + +You can utilize the `externalId` argument to establish a connection between an account in database and an Enjin Platform Account. + +It's worth noting that you have control over setting a user's `externalId`, so it's advisable to choose an identifier that aligns with your existing database, making "`externalId`" a valuable and versatile option for processing requests. + +## signature + +Allows for message authentication, ensuring that data remains unaltered during transmission, and guarantees the integrity of blockchain-stored data. + +Additionally, `signature` serves as a means of verifying user identity and authorization, providing non-repudiation, enhancing security, and confirming the legitimacy of blockchain transactions. + +Overall, this argument is indispensable for maintaining trust, security, and data reliability in cryptographic and blockchain environments. + +## uuids + +You would use the "uuids" argument in an argument to acknowledge and remove specific events from a cache, ensuring cache integrity and efficient system operation by eliminating unnecessary data. + +## expiration + +You use the `expiration` argument when authorizing another account to manage tokens from a specific collection for a limited time, ensuring that the approval automatically expires at the specified block number, providing control and security in scenarios involving NFTs or tokenized assets without a permanent transfer of ownership. + +## operator + +Use the `operator` argument to grant specific accounts the authorization to manage and operate a token collection, allowing for delegated token management without transferring ownership. + +## signingAccount + +Use the `signingaccount` argument to specify the account responsible for signing and authorizing a particular action or transaction, ensuring security, accountability and traceability in the process. + +## simulate + +You would use the `simulate` parameter set to `true` when you want to test and validate the outcome of a transaction without actually executing it on the blockchain network. + +It provides a valuable way to safely experiment with transactions and prevent unintended consequences, especially in scenarios where accuracy and precision are critical. + +## recipients + +You can use the **recipients** parameter for transferring or minting tokens, to specify the accounts to which the tokens will be transferred, enabling targeted and controlled token transfers to designated recipients. + +## attributes + +use on-chain attributes to assign and manage metadata, enabling detailed and comprehensive token metadata definition, which is valuable for providing rich and descriptive information about the token on the blockchain. + +### key + +A "key" serves as the identifier or label for a specific attribute. + +### value + +A "value" represents the corresponding information or data associated with that attribute. + +## hasRoyalty + +Configures royalties for each marketplace sale of this token. +Royalties are effectively enforced by the core blockchain, ensuring you receive your entitled earnings whenever the token is involved in transactions, providing a transparent and automated system for royalty distribution. + +- #### beneficiary + + - The recipient or entity designated to receive a portion of the proceeds from token sales + +- #### percentage + + - Represents the specific portion or share of the transaction value that is allocated to the beneficiary. + +## isCurrency + +This concept is used by the marketplace to determine what assets are allowed to be listed, and which side of the listing is charged fees. + +## cap + +Employed to limit the maximum supply of tokens, creating scarcity and rarity by setting an upper bound on the number of tokens that can ever be created. + +- #### SUPPLY + + - A fixed supply model where the cap amount sets the maximum tokens that can exist in circulation. Burned tokens can be re-minted. + +- #### COLLAPSING_SUPPLY + + - A dynamic supply model where the cap amount defines the total tokens that can ever be minted. Burned tokens cannot be re-minted. + +- #### SINGLE_MINT (DEPRECATED) + + - Allows the token to be minted only once. Functions as a collapsing supply model with the cap amount set to the initial supply. + +- #### INFINITE + + - Allows unlimited token creation with no supply cap. + +### type + +defines the token's minting behavior, where `INFINITE` allows unlimited minting, `SINGLE_MINT` enables minting of a single token, and `SUPPLY` specifies a fixed supply of tokens. + +## amount + +Represents a numerical value associated with the quantity of a token, such as the quantity available, the total supply, or the number of tokens to be minted, depending on the context of its use. + +## freezeState + +Allows you to define the duration of the suspension of transferability, with `PERMANENT` indicating a long-term freeze, `TEMPORARY` signifying a short-term suspension, and `NEVER` indicating that the token or collection will never be frozen, providing flexibility in asset management. + +## initialSupply + +Specifies the quantity of tokens that will be minted and delivered to the designated recipient. + +## listingForbidden + +Setting `listingForbidden` to true indicates that the token is restricted from being listed on a marketplace, providing control over its availability and preventing it from being traded or displayed on external platforms. + +## unitPrice + +The `unitPrice` of a token is critical as it determines the cost structure, ENJ backing, and accessibility of the token, with the added significance that it can be increased but never decreased, requiring alignment with the token's distribution goals. + +## signingAccount + +The "signingAccount" parameter should be configured with the address of the wallet that owns the token, ensuring that the actions and transactions associated with the token are appropriately authorized and executed by the correct account. + +## skipValidation + +The `skipValidation` option, when set to true, allows for the bypassing of validation checks, which should be exercised with caution and only in scenarios where custom validation or specific requirements necessitate the bypassing of standard validation rules. + +## continueOnFailure + +The `continueOnFailure` parameter, when set to true, indicates that the system should proceed with the transaction even if it encounters a failure or error during execution, offering a graceful handling of errors and allowing the transaction to continue despite issues. When set to false, any failure during execution will result in the transaction being halted and not proceeding further. + +## keepAlive + +When conducting Enjin Coin (ENJ) transfers, utilizing the "keepAlive" option with a value of true is advisable if you intend to preserve the sender's Enjin Coin Account, even when its balance drops below the minimum threshold of 0.1 ENJ. + +It's important to recognize that Enjin Coin Accounts and Token Accounts are distinct entities, meaning that a wallet can still hold tokens and NFTs even if the Enjin Coin Account is removed. + +## source + +The `source` parameter refers to the address from which the tokens will be transferred, representing the ownership address of the tokens and the origin of the transfer. + +## mintPolicy + +The "mintPolicy" parameter allows you to define the minting policy for tokens within a collection, with the following sub-parameters: + +- #### forceCollapsingSupply + + - Determines whether the tokens in this collection will be minted as **CollapsingSupply** types. This supply type allows the collection owner to mint new tokens as long as the token's circulating supply does not exceed it's max supply. Burning tokens reduces the max supply, meaning that in collapsing supply type, burned tokens cannot be re-minted. + +- #### maxTokenCount + + - Specifies the maximum number of tokens that can be issued within this collection, setting an upper limit on the total token count. + +- #### maxTokenSupply + + - Sets the maximum quantity of each unique token within this collection that can be minted, establishing an upper limit on the available supply for individual tokens. + +## marketPolicy + +Configures default marketplace royalties for all tokens in the collection. + +- #### royalty + + - A default royalty percentage for all tokens in the collection. This can be overridden on a per-token basis. + +## explicitRoyaltyCurrencies + +The `explicitRoyaltyCurrencies` field within a collection serves the purpose of specifying which currencies are permitted to be accepted as royalties. + +By including specific currencies in this field, a collection restricts the acceptance of royalties to only those currencies, ensuring that any listing created that intends to pay royalties in an unaccepted currency will result in a failed listing creation process. + +This feature enhances control and compliance in managing royalties within the collection. + +## freezeType + +"Freezing" refers to the process of temporarily suspending the transferability of a collection or a specific token. + +The `freezeType` parameter offers flexibility in specifying what can be frozen within the blockchain system, with the following options: + +- "COLLECTION": Indicates that an entire collection of tokens can be frozen. + +- "COLLECTION_ACCOUNT": Allows for the freezing of an entire collection account. + +- "TOKEN": Permits the freezing of individual tokens. + +- "TOKEN_ACCOUNT": Allows for the freezing of an entire token account. + +## markAsProcessing + +The "markAsProcessing" action involves retrieving a list of newly pending transactions and subsequently marking them as "processing," indicating that they are currently being handled or executed within the blockchain system. + +## owner + +The term "owner" can be employed in various contexts, depending on the specific action being performed. + +For example, when unapproving a collection, "owner" refers to the account from which the collection approval will be removed, signifying the entity or account that holds ownership or control over the approval status of the collection. + +# Beam + +## TokenIds + +Specifies a list of integer s claimable in the beam. A Beam Claim will be generated for each provided ID. +Provide IDs as strings, using commas to separate multiple entries. Supports single IDs (e.g., "1"), ranges (e.g., "6..8"), or a combination (e.g., ["1", "6..8", "10"] includes IDs 1, 6, 7, 8, and 10). + +## tokenQuantityPerClaim + +Specifies how many token units will be minted or transferred for each Beam Claim. + +## claimQuantity + +Specifies the number of Beam Claims to create for each provided token ID. Defaults to 1. + +## type {#beam-claim-type} + +Specifies how tokens are delivered when the beam is claimed: + +- #### TRANSFER_TOKEN + + - Transfers existing tokens to the recipient. + +- #### MINT_ON_DEMAND + + - Mints tokens directly to the recipient. + +## claimLimit + +Specify the number of times each user can claim the beam. + +## flags + +Specifies the type of beam behavior to modify. Available options include: + +- #### PAUSED + + - Temporarily prevents claims from being processed. + +- #### SINGLE_USE + + - Enables the use of single-use claim codes for distributing specific tokens from the beam. + +- #### PRUNABLE + + - Allows claim records for expired beams to be deleted from the database after a set period. \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/_category_.json b/versioned_docs/version-2.x/03-api-reference/_category_.json new file mode 100644 index 0000000..950677a --- /dev/null +++ b/versioned_docs/version-2.x/03-api-reference/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "API Reference" +} \ No newline at end of file diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/01-validator-staking.md b/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/01-validator-staking.md new file mode 100644 index 0000000..410ab5c --- /dev/null +++ b/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/01-validator-staking.md @@ -0,0 +1,125 @@ +--- +title: "Validator Staking" +slug: "validator-staking" +--- +This is an overview of staking on Enjin Relaychain, known as liquid staking. + +:::info Staking Interfaces +- **[NFT.io](https://nft.io):** Use NFT.io for a simplified experience. It provides a convenient, user-friendly interface to manage your stake and nomination pools. +- **[Enjin Console](https://console.enjin.io/)**: For advanced features and the full user interface referenced in this document, use [console.enjin.io](https://console.enjin.io/). + - *Note:* Ensure you connect to the **Enjin Relaychain** network in the top left corner, as staking takes place on the Relaychain. +::: + +## Concepts + +- **Direct staking -** A user stakes directly to one or more validators and is paid directly by them. +- **Nomination Pool (indirect staking) -** One account stakes funds that represent a group of users. This account handles the nominating and receives the rewards. +- **Liquid staking -** a token is received that represents the user's stake. The staked token can be used (is liquid) while the user is staked. It is minted when the user stakes and burned when they unstake. It can also be exchanged for the real token. +- **Validator -** Provides security for the network and receives rewards for doing so. On Enjin, these rewards are distributed to nomination pools. (A list of active validators can be seen on [Subscan](https://enjin.subscan.io/validator).) +- **Nominator -** The account that chooses a validator to stake with. In the case of Enjin, this is the nomination pool's account. +- **Commission -** A percentage of the rewards taken for offering a service. This can be either or both: a commission set by a validator, for their contributions to progressing and securing the network; and/or a commission set by a nomination pool owner, for having created the pool for which the active validator(s) were selected and the user(s) of the pool benefited. + +## Nomination Pools vs Direct Staking + +On Enjin, we use nomination pools to handle staking due to the increased simplicity and lower minimums. Another type of staking supported on substrate is direct staking, where a user stakes directly to a validator, but it has several disadvantages. + +### Nomination Pools with liquid staking + +With nomination pools, there is one account that multiple users put their funds into. This account acts as the nominator and receives the rewards. The liquid token, sENJ, is used to represent a user's stake in the pool. + +- **Low minimum (1 ENJ) -** pools can have lower minimums per user because all of the pool's funds are staked together +- **Easy to use** - users only need to choose a pool, no need to choose validators +- **Receive liquid token -** a liquid token, sENJ, is received that represents the user's stake. This can be transferred or even exchanged for ENJ +- **Immediate exit -** due to the liquid token, the user can exit immediately by exchanging their sENJ for ENJ + +

+ +

+ +### Direct Staking + +:::note +Direct staking is not supported on Enjin. This information is just for comparison. +::: + +- **High minimum -** direct staking has a high minimum balance. On polkadot, it's 250 DOT. +- **Must choose validators -** each validator must be manually chosen. This can be difficult for new users. +- **Staked token is locked -** the staked token cannot be used while staked +- **Stash / controller accounts -** multiple accounts must be created, increasing complexity +- **28 days unbonding period -** users must wait 28 days after unbonding to receive their funds + +

+ +

+ +## Tokens Overview + +There are three tokens relevant to staking: + +- **ENJ -** ENJ is the native token of the Enjin network. It is the token staked in the pool. +- **sENJ -** sENJ is the liquid token that represents staked ENJ. It is minted when ENJ is staked and burned when ENJ is unstaked. It can also be exchanged for ENJ through the `stakeExchange` pallet. +- **Degen Token -** The degen token is an NFT in a special collection. It is required to create a pool and is also used to manage the pool. The owner of this NFT can optionally receive a commission. Each token is only usable in one pool at a time. + +![](/img/components/enjin-relaychain/3.png) + +## Deposit + +A deposit is reserved when the pool is created. This deposit will be released to the holder of the degen token when the pool is destroyed. Here's an example: + +1. Alice creates a pool with degen 3 and 5000 ENJ + 1. ENJ is locked in a pool as deposit + 2. Degen 3 is associated with the pool + 3. Alice receives commissions +2. Alice transfers degen 3 to Bob + 1. Bob begins receiving commissions + 2. Bob can now manage the pool +3. Bob destroys the pool and everyone withdraws + 1. Bob gets the 5000 ENJ deposit because he holds the degen + 2. Bob would actually get slightly more than 5000 ENJ because the deposit is staked, so he would get the rewards as well + +![](/img/components/enjin-relaychain/4.png) + +## ENJ & sENJ + +Here's an example to show the relationship between ENJ and sENJ. The important thing to understand is that although the number of sENJ does not change as rewards are distributed, its value increases because it represents a proportion of the total ENJ in the pool. + +![](/img/components/enjin-relaychain/5.jpg) + +## Payouts + +Reward payouts are automated by validators, eliminating the need for manual intervention. However, understanding the distribution logic is helpful: + +1. **Reward Collection:** Staking rewards are collected from the validator into the reward account for the current era. +2. **Reward Distribution:** Rewards are distributed for the *previous* era (one era lag) to ensure all validator rewards are fully collected first. This stage involves: + 1. Sending the commission to the Degen token holder. + 2. Staking (reinvesting) the pool's remaining reward balance to maximize future returns. + +![](/img/components/enjin-relaychain/8.png) + +## Extrinsics Overview + +Here's an overview of the extrinsics (operations) in pallet-nomination-pools. + +### Administration + +These extrinsics are for creating or modifying pools and require special permissions. Some must be called directly by the owner of the degen NFT, and a few can be called if assigned to a specific role. + +- **`create`** - creates a new pool. Requires the degen NFT and the deposit. +- **`chill`** - stops the pool from receiving rewards. +- **`destroy`** - puts the pool into the destroying state +- **`nominate`** - nominates validators for the pool +- **`mutate`** - makes changes to the pool + +### Members + +These are the extrinsics that will be called by users (members) of the pool. + +- **`bond`** - bonds ENJ to the pool. The user receives sENJ. +- **`unbond`** - Unbonds sENJ. The user burns sENJ. +- **`withdraw_unbonded`** - The ENJ is received from unbonding after the unbonding period has passed. + +### Permissionless + +These extrinsics can be called when the pool is in the `Destroying` state, by anyone. +* **`unbond_deposit`** - Unbonds the deposit. Only callable after the pool is destroyed and all members have left the pool. +* **`withdraw_deposit`** - Withdraws the deposit. Can only be called after the deposit is unbonded. The pool will be destroyed (deleted) after this is called successfully. \ No newline at end of file diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/02-enjin-relaychain.md b/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/02-enjin-relaychain.md new file mode 100644 index 0000000..6ab0d13 --- /dev/null +++ b/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/02-enjin-relaychain.md @@ -0,0 +1,35 @@ +--- +title: "Enjin Relaychain" +slug: "../enjin-relaychain" +description: "Layer 0: The security layer of the Enjin Blockchain." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +## What is Enjin Relaychain? + +Enjin Relaychain is a decentralized and scalable network built for digital assets and non-fungible tokens (NFTs). It is based on Polkadot and uses the open-source Substrate framework to provide a comprehensive and efficient ecosystem for NFT creation. With superior functionality, speed, and low costs, Enjin Blockchain is designed to enhance the creation and management of digital assets in a secure and reliable manner. + +

+ +

+ +## Benefits of the Enjin Relaychain: + +1. Scalability: Enjin Relaychain is built to be highly scalable, enabling a significant increase in transaction throughput and enhanced performance for NFT projects, games, brands, and communities around the globe. This scalability is achieved through a unique architecture that leverages a multi-chain approach, allowing for parallel processing of transactions, reducing bottlenecks, and optimizing the use of resources. +2. Customizable Matrix Chains: Enjin Relaychain features a primary Relay chain and several Matrix Chains, which can be customized to meet the specific needs of different use cases. The Relay chain is optimized for security, network synchronicity, and block validation, while the Matrix Chains are specialized blockchains that can prioritize speed, scalability, and cost efficiency. By offloading security and integrity to the Relay chain, Matrix Chains can evolve and adapt to changing market demands without compromising their unique features or properties. This architecture provides the flexibility and agility needed for NFT projects, games, brands, and communities to innovate and thrive. +3. Predictable Scalability and Costs: Enjin Blockchain takes a different approach to scalability and costs than other blockchain networks by creating an interoperable network of Matrix Chains with predictable scalability and costs. Unlike the auction-based parachain lease model used in Polkadot, Enjin's approach eliminates the hindrances and risks associated with parachain auctions and high development costs. This makes it easier and more cost-effective to launch and upgrade Matrix Chains, providing a predictable and reliable environment for developers and businesses to build on. +4. Streamlined Staking: Enjin Relaychain uses a nominated proof-of-stake mechanism on the Relaychain to maintain security. Staking is simplified through an intuitive user interface that is accessible directly from the Enjin Wallet. Users can easily allocate Enjin Coin to a curated nomination pool with just one click, removing the need for manually selecting individual nodes. The staked amount remains highly liquid and can be used within the Enjin ecosystem, without requiring it to be unbound from the nomination pool beforehand. This ensures the coin's utility and enables users to participate in securing the network seamlessly. +5. Inclusivity and Accessibility: Enjin Blockchain is founded on the principles of inclusivity and accessibility. It provides an opportunity for anyone to participate in network security and earn Enjin Coin rewards. With pre-configured packages, anyone can deploy a , Node, or Nomination Pool without the need for complex technical knowledge, making it easy for users to contribute to the network. This approach ensures that the network is open and accessible to all, promoting inclusivity and community involvement. +6. Seamless Integration with Enjin Products: Enjin Relaychain seamlessly integrates with popular Enjin products, such as the Enjin Wallet, NFT.io - NFT Marketplace, Enjin Platform, Enjin Beam, Enjin Web Wallet, Enjin Platform SDKs, and more. This provides a comprehensive and end-to-end integrated ecosystem, delivering a smooth experience for both users and developers. The integration ensures that all Enjin products leverage the benefits of the Relaychain, including scalability, low costs, and increased speed. + +Enjin Relaychain is a decentralized, multi-chain network that aims to provide a superior digital asset ecosystem for NFT projects, games, brands, and communities globally. Its nominated proof-of-stake staking structure guarantees regulatory compliance and security. Enjin Relaychain offers the following key features and benefits: + +- Scalable and customizable multi-chain network; +- Enhanced NFT creation capabilities; +- Predictable scalability and costs; + User-friendly staking; +- Inclusive and accessible for all users; +- Seamlessly integrated with Enjin products. + +Overall, Enjin Relaychain offers a secure and efficient platform for creating and managing NFT projects, games, and communities. Its features and benefits are designed to make the process of creating and managing digital assets more accessible, cost-effective, and efficient while ensuring regulatory compliance and security. diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/02-stake-exchange-pallet.md b/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/02-stake-exchange-pallet.md new file mode 100644 index 0000000..c34536b --- /dev/null +++ b/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/02-stake-exchange-pallet.md @@ -0,0 +1,175 @@ +--- +title: "Stake Exchange Pallet" +slug: "stake-exchange-pallet" +--- +:::info The Enjin Blockchain Console +Use [console.enjin.io](https://console.enjin.io/) to use the user interface referenced in this document. +::: + +## Introduction + +Stake Exchange Pallet enables users holding ENJ tokens to create offers for staking and allows users holding sENJ tokens to exchange them back to ENJ tokens. Each offer has preferences regarding the exchange rate and the specific type of sENJ tokens it can accept. It's important to note that sENJ tokens correspond to a different pool in the Stake Exchange Pallet. Users can select the offer that best suits their preferences and perform the token exchange accordingly. This user manual provides step-by-step instructions on how to use the pallet to stake ENJ tokens and exchange them for sENJ tokens. + +## Enjin Nomination Pools and Liquid Staking: + +Enjin Nomination Pools are an integral part of the Stake Exchange Pallet. They serve as pools that hold ENJ tokens, and issue sENJ tokens in return. Each sENJ type corresponds to a specific pool. + +Liquid Staking allows users to stake their ENJ tokens and receive sENJ tokens in return. These sENJ tokens represent the staked value of their ENJ tokens. By staking their ENJ tokens, users contribute to the security and operation of the Enjin network. + +Through the Stake Exchange Pallet, users holding sENJ tokens can easily convert them back to ENJ tokens by selecting the appropriate offer from the available options. Each offer within the pallet specifies the exchange rate and the type of sENJ tokens it accepts. Users can choose the offer that aligns with their preferences and execute the token exchange accordingly. + +By utilizing Enjin Nomination Pools and the Stake Exchange Pallet, users have the flexibility to stake their ENJ tokens for network participation and later convert them back to ENJ tokens when desired, providing liquidity and accessibility to their staked assets. + +## Glossary + +**sENJ Tokens**: The staked version of the ENJ tokens. These tokens are represented using the `StakedCollectionId` collections in the `pallet_multi_tokens` instance. + +**Exchange Rate**: The rate at which ENJ tokens can be exchanged for sENJ tokens and vice versa. + +**Liquidity Account**: An account used for managing liquidity. It is associated with a specific user and holds the liquidity for their offers. + +## Getting Started + +Ensure that you have access to an Enjin Relay Chain and have the necessary ENJ tokens to stake. The UI used in this guide can be accessed [here (*Canary Testnet)](https://console.enjin.io/?rpc=wss%3A%2F%2Frpc.relay.canary.enjin.io#/extrinsics) + +**Accessing the Pallet Functions:** +Open your Enjin Explorer and navigate to the Developer -> Extrinsics interface. Locate the "Stake Exchange Pallet" and click on it to access the available functions. + +## Creating an Offer + +- Click on the "Create Offer" function to start the process of staking your ENJ tokens and creating an offer. +- Enter the required parameters for the offer: + - Total : Specify the amount of ENJ tokens you want to exchange. + - Rate : Define the exchange rate for converting sENJ tokens to ENJ tokens. + - minAverageRewardRate : Define the minimum reward rate you would accept for the offer. +- Review the entered parameters and click "Confirm" to proceed. +- If successful, you will receive a confirmation message indicating that the offer has been created. Note the generated Offer ID for future reference. + +*** + +**_Note_** : `PoolReward` is calculated by dividing the ENJ bonded in the pool by the points in the pool. Lets consider these two examples + +Pool X (has not received any rewards): + +| Points | Total Bonded ENJ | Avg reward rate | +| ------ | ---------------- | --------------- | +| 1000 | 1000 | 1 | + +Pool Y : (received some rewards) + +| Points | Total Bonded ENJ | Avg reward rate | +| ------ | ---------------- | --------------- | +| 1000 | 1100 | 1.1 | + +Pool Z : (slashed) + +| Points | Total Bonded ENJ | Avg reward rate | +| ------ | ---------------- | --------------- | +| 1000 | 900 | 0.9 | + +*** + +**_Note_** : `minAverageRewardRate` is used to represent the minimum accepted reward rate of a token (or pool). The reward rate of the pool is calculated by dividing the ENJ bonded in the pool by the points in the pool. By setting this value, the user can ensure a minimum reward rate for all sENJ tokens used to fill this offer. Lets consider two examples below + +1. minAverageRewardRate set to 1.1 + +In this case, only Pool Y tokens (and any other pool where average reward rate is greater than 1.1) will be accepted. If a user with sENJ tokens from Pool Y tries to buy from this offer, they will see the `RewardRateOutOfRange` error. + +2. minAverageRewardRate set to 1 + +In this case, both Pool X and Pool Y tokens (and any other pool where average reward rate is greater than 1) will be accepted. If a user with sENJ tokens from Pool Z with a reward rate of 0.9 tries to buy from this offer, they will see the `RewardRateOutOfRange` error. + +So as the creator of the offer, it is in your best interest to keep this value as high as possible, so that the sENJ you will receive will reap the maximum rewards. Do keep in mind that there are practical limitations in how high this value can be set, for example if you set it to `3` (aka 3x reward for every token), no sENJ tokens maybe eligible to fill your offer since it is unlikely to have a pool/validator with that high reward rate. The ideal value to set here can be obtained by inspecting the current active pools, and setting a value that is high enough and accepts most pools. + +*** + +**_Note_** : The `minAverageRewardRate` is represented by u128, which allows for higher precision. Example : + +if its 1, (the pool should have a reward rate greater than or equal to 1) you would use 1000000000000000000 (1x) +if its 1.1, (the pool should have a reward rate greater than or equal to 1.1), you would use 1100000000000000000 (11.x) +if its 2 (the pool should have a reward rate greater than or equal to 2), you would use 2000000000000000000 (2x) + +To get the input ratio, you can multiply your desired ratio with `1000000000000000000`, +ex : `1.112 * 1000000000000000000 = 1120000000000000000` +This ratio means, the offer will only accept pool with a reward rate more than 1.112 + +*** + +**_Note_** : The `rate` is also represented by Perbill, this value will be used to determine how many `sENJ` will be required to get `ENJ`. Example : + +if its 1:1, (you need 1 sENJ to get 1ENJ) you would use 1000000000 (100%) +if its 0.9:1, (you need 0.9 sENJ to get 1ENJ), you would use 900000000 (90%) + +*** + +**_Note_** : The `tokenFilter` is used to control which pool tokens can be used to fill an active offer. TokenFilter can be configured to be one of these values + +1. All (all pool tokens are accepted, configs set on liquidity account still apply) +2. Whitelist (this allows you to whitelist pool tokens, only these tokens will be accepted, all others will be rejected). +3. Blacklist (this allows to block selected pool tokens, all other tokens will be accepted.) + +*** + +The below example creates an offer with a liquidity of 1000ENJ (1000000000000000000000), and offers a rate of 99% (990000000), and will accepts pools with a reward rate of 1.1 (1100000000000000000). The offer only accepts tokens from Pool ID 2. + +![](/img/components/enjin-relaychain/34.png) + +## Canceling an Offer + +- If you wish to cancel a previously created offer, click on the "Cancel Offer" function. +- Enter the Offer ID of the offer you want to cancel. +- Click "Confirm" to proceed with the cancellation. +- If successful, you will receive a confirmation message indicating that the offer has been canceled. + +![](/img/components/enjin-relaychain/35.png) + +## Configuring Liquidity Account + +:::note +Configuring Liquidity Account is only required if you create an offer aka if you provide liquidity to the pallet, you do not need to configure a liquidity account to buy tokens +::: + +- To configure your liquidity account, click on the "Configure Liquidity Account" function. +- The liquidity account configuration allows you to allow/disallow certain sENJ tokens from the offer created by your account. To allow an sENJ token, add the token to the whitelist and to disallow it, add it to the blacklist. These restrictions are enforced to all offers created by your account. + + Example configuration to disallow sENJ tokens with tokenId 210: + +![](/img/components/enjin-relaychain/36.png) + +## Withdrawing Liquidity + +- If you want to withdraw liquidity from an active offer, click on the "Withdraw Liquidity" function. +- Enter the Offer ID of the offer from which you wish to withdraw liquidity. +- Specify the amount of liquidity you want to withdraw. +- Click "Confirm" to proceed with the withdrawal. +- If successful, you will receive a confirmation message indicating that the liquidity has been withdrawn. + + Example : You created an offer with initial amount of 1000ENJ. If you withdraw liquidity of 400ENJ, then your updated offer will have a liquidity of 600ENJ. + +![](/img/components/enjin-relaychain/37.png) + +## Adding Liquidity + +- To add liquidity to an active offer, click on the "Add Liquidity" function. +- Enter the Offer ID of the offer to which you want to add liquidity. +- Specify the amount of liquidity you want to add. +- Click "Confirm" to proceed with adding liquidity. +- If successful, you will receive a confirmation message indicating that the liquidity has been added. + + Example : You created an offer with initial amount of 1000ENJ. If you add liquidity of 400ENJ, then your updated + offer will have a liquidity of 1400ENJ. + +![](/img/components/enjin-relaychain/38.png) + +## Buying Tokens + +- To exchange sENJ tokens for ENJ tokens, click on the "Buy" function. +- Enter the Offer ID of the offer from which you want to buy tokens. +- Specify the amount of ENJ tokens you want to receive. +- Enter the Token ID for the specific token you want to exchange. +- Click "Confirm" to proceed with the token exchange. +- If successful, you will receive a confirmation message indicating that the token exchange has been completed. You will also receive the exchanged ENJ tokens in your wallet. + +![](/img/components/enjin-relaychain/39.png) + +Happy staking and token exchanging! diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/03-slashing.md b/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/03-slashing.md new file mode 100644 index 0000000..9406408 --- /dev/null +++ b/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/03-slashing.md @@ -0,0 +1,49 @@ +--- +title: "Slashing" +slug: "slashing" +description: "Slashing is a mechanism designed to ensure the integrity, security and reliability of the Enjin Blockchain network by penalizing validators for malicious or negligent behavior. This mechanism is crucial to maintaining trust and fairness within the network by discouraging actions that could compromise its safety or availability." +--- +## Causes of Slashing + +Slashing occurs when a validator commits an offense that goes against the Enjin Blockchain’s rules. Offenses that could lead to slashing on the Enjin Blockchain include: + +- **Double Signing (Equivocation):** A validator signs two different blocks at the same block height, potentially forking the blockchain and undermining the consensus mechanism. +- **Unresponsiveness:** A validator is offline or fails to produce blocks or participate in consensus rounds over a defined period, negatively impacting network performance and security. +- **Invalid State Transitions:** Proposing blocks containing invalid state transitions or transactions that do not comply with network rules can result in slashing. + +## Slashing Levels + +Slashing penalties are categorized into different levels depending on the severity of the offense: + +- **Minor Offense:** Includes brief periods of downtime or other non-malicious behavior. Penalties for minor offenses typically involve a small reduction in staked ENJ (0 - 1%). +- **Major Offense:** Encompasses more serious behaviors, such as repeated equivocation or failure to validate correctly over extended periods.These offenses carry harsher penalties, potentially resulting in moderately medium reductions in staked ENJ (1 - 10%) and possible exclusion from validator duties. +- **Critical Offense:** Involves malicious actions, such as double signing or producing invalid blocks, that directly threaten network security. Validators committing critical offenses may face severe penalties, including the loss of a substantial portion or all of their staked ENJ (10 - 100%) and immediate removal from the active validator set. + +## Slashing Process + +When a validator is found to have committed an offense, the slashing process is initiated: + +- **Detection:** The offense is detected by the Enjin Blockchain through automated mechanisms or reports from other validators and network participants. +- **Verification:** The detected offense is verified through consensus among the active validator set or via automated checks performed by the network. +- **Execution:** Once the offense is verified, the slashing is executed automatically by the network. The validator’s staked ENJ is reduced according to the severity of the offense, and the validator may be temporarily or permanently removed from the active validator set. + +### Slashed Tokens + +When slashing occurs, a portion of the validator’s staked ENJ tokens, including those of the nominators who have delegated their tokens to the validator, is forfeited. This forfeited amount is then transferred to the [Enjin Treasury](https://enjin.subscan.io/account/enD9wdMEaQa3MEDUc7dtsCC86JYGMN5JBE2NBRoMyC37dX4iA). The rationale for transferring slashed tokens to the Treasury rather than burning them or redistributing them as rewards is twofold: + +- **Reversal of Faulty Slashing:** Slashing penalties can be reverted by paying out from Treasury in cases where the slashing was deemed erroneous or unjustified. +- **Supporting Ecosystem Growth:** In the case of legitimate slashing, the tokens are redirected from malicious validators to support ecosystem development through the standard Treasury process, rewarding those actively contributing to the growth and health of the Enjin Blockchain. + +It’s important to note that slashing only affects active nominations for a given nominator. Slashes are not mitigated by having other inactive or waiting nominations. Furthermore, slashing penalties apply independently to each validator node operated by a validator, with each node being considered a separate entity for slashing purposes. + +#### Impact on Nominators + +Nominators who delegate their ENJ tokens to a validator are also at risk of slashing if the chosen validator commits an offense. The slashing penalty is distributed proportionally across the validator’s own stake and the stake of their nominators. Therefore, nominators should carefully choose validators with a proven track record of reliability and adherence to network rules. + +## Good Practices to Avoiding Slashing + +Validators on the Enjin Blockchain must follow best practices to avoid slashing penalties and ensure the security and reliability of nodes. You can refer to the [Security Best Practices for Validators](/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/02-security-best-practices-for-validators.md) article for more information. + +## Conclusion + +Slashing on the Enjin Blockchain is an essential mechanism to maintain network security, integrity and reliability by discouraging malicious or negligent behaviors of validators. By understanding the causes, levels and process, validators and nominators can better navigate the network, manage risk and contribute to a healthy and secure ecosystem. diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/_category_.json b/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/_category_.json new file mode 100644 index 0000000..632c87b --- /dev/null +++ b/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Enjin Relaychain" +} + \ No newline at end of file diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md b/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md new file mode 100644 index 0000000..5c8c5ee --- /dev/null +++ b/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md @@ -0,0 +1,328 @@ +--- +title: "MultiTokens Pallet" +slug: "multitoken-pallet" +description: "The utility pallets of the Enjin Blockchain." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +:::info The Enjin Blockchain Console +Use [console.enjin.io](https://console.enjin.io/) to use the user interface referenced in this document. +::: + +## What is the MultiTokens ? + +The Enjin Blockchain has a token standard called MultiTokens. The standard is compatible with matrixchains, parachains, parathreads and smart contracts, so it’s interoperable with the entire Enjin, Polkadot and Kusama ecosystem. + +**** are stackable, have a quantity and optional decimal places. An example of a multi-unit token is a twenty dollar bill - each bill is worth the same amount as another twenty dollar bill. + +![](/img/components/enjin-matrixchain/1.png) + +**s** are not interchangeable; each token has its own unique identifier. Examples of NFTs are original art, gaming characters and pets, numbered collectibles, and more. + +![](/img/components/enjin-matrixchain/2.png) + +**Grouped Tokens** are Multi-unit tokens or NFTs organized into logical sub-categories within a collection. This adds a structural layer between the collection and individual tokens, allowing for organized "folders" in apps like the Enjin Wallet and NFT.io. + +![](/img/components/enjin-matrixchain/3.png) + +:::info Partial Implementation +Token grouping is currently implemented at the blockchain level. While users can view and manage groups on NFT.io and view them in the Enjin Wallet, full Enjin Platform (API/UI) support is currently in development. +::: + +## Terminology + +- **Collection -** A group of tokens. Also holds data for those tokens and the policies that govern their behavior. +- ** -** An organizational layer within a collection used to categorize tokens. +- **Token -** A unique asset with a balance +- **Token Account -** A token account is stored in user account. It holds the account's states like its balance, freeze state, etc. +- **Policy -** Governs behavior for tokens in a collection +- **Attribute -** Metadata (strings/data) attached to a collection, group, or token. +- **Operator -** An account that operates on behalf of another account (transferFrom) +- **Approval -** Required for an operator to use an account +- **Freeze/Thaw -** If a collection, token, or account is frozen, it cannot transfer tokens +- **Descriptor -** Used to create something. For example, a CollectionDescriptor creates a Collection. + +## Collections + +A collection must be created before tokens may be minted. A collection is somewhat akin to an ERC-1155 smart contract - both and tokens can be created in a single collection, and its creator has certain privileges, such as minting new tokens or setting metadata for the collection and its tokens. + +The first 2000 Collection IDs are reserved for future system collections. Collections created on-chain through this start from ID 2001 and are sequentially created. + +A of 6.25 ENJ is required to create a collection. The deposit can be recovered by the collection owner if all tokens are burned and the collection is destroyed. + +![](/img/components/enjin-matrixchain/4.png) + +![](/img/components/enjin-matrixchain/5.png) + +![](/img/components/enjin-matrixchain/6.png) + +## Token Grouping + +Token Grouping adds an organizational layer between a **Collection** and its **Tokens**. It allows creators to categorize assets into logical "folders," significantly improving navigation and user experience. + +For example, a "Fantasy RPG" collection can group its tokens into "Weapons," "Armor," and "Potions." Within "Weapons," tokens could be further organized into "Swords" or "Axes." Without grouping, all tokens appear in a single, flat list, making specific items difficult to locate. + +### Functional Overview +* **Blockchain Level:** Groups are implemented directly on the MultiTokens pallet. +* **Attributes:** Like collections, groups can have their own attributes (metadata). +* **Organization:** In the Enjin Wallet and NFT.io, grouped tokens are displayed in folders rather than a random sorted list. + +### Implementation Status +Token grouping is currently in a **partial release** phase: +* **Enjin Wallet:** Users can view and navigate tokens organized into group folders. +* **NFT.io:** Users can view groups and manage them (manual or auto-grouping). +* **Enjin Platform:** UI and API support for managing groups via the Platform is currently in development. + +## Tokens + +Each token must belong to a Collection, and is created using the mint extrinsic. + +In the Enjin Blockchain there is no distinction between s and s. Each token is a that may be a multi-unit token or an NFT depending on its supply. A NFT is simply a token with a total supply of one. Additional constraints, like a cap, can be applied to a token at the time of minting to make sure the total supply never increases. + +![](/img/components/enjin-matrixchain/7.png) + +## How to Create a Collection + +A collection is created by using the `create_collection` extrinsic. The only required parameter is a descriptor which allows customizing the collection's behavior. The descriptor contains all of the policies and some additional settings. Some settings can be changed later using the `mutate_collection` extrinsic, but the policies currently cannot be changed, so think carefully before choosing the policies. + +By default, the policies are flexible, but you can make them more strict. For example, if you want to force a collection to only contain NFTs, you can set `max_token_supply` to `1` and `force_collapsing_supply` to true on the mint policy. + +### Create Collection Parameters + +- Max Token Count: The maximum number of unique tokens that can exist within the collection, regardless of the supply of each token. Setting this to `None` allows for the creation of unlimited number of unique tokens within the collection. +- Max Token Supply: The maximum supply limit for each individual token within the collection. Setting this to 1 will ensure each token within the collection is an NFT. +- Force Collapsing Supply: Whether all tokens within the collection should have a collapsing supply. Read more on the available supply types in the [#Create Token Parameters](#create-token-parameters) section below. +- Market Policy: Whether each token within the collection should have forced marketplace royalties. Read more in the [#Create Token Parameters](#create-token-parameters) section below. +- Explicit Royalty Currencies: List of tokens that will be allowed to be used as a royalty currency for each filled listing of a token withing the collection. If no tokens are provided, all tokens will be allowed to be used as a royalty currency. + Read more in the [#Create Token Parameters](#create-token-parameters) section below. +- Attributes: The collection's attributes such as `name`, `description`, `media`, etc. These should be structured according to the [metadata standard](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md). + +When the collection is created, it will emit a `CollectionCreated` event. This event contains the collection id that is used to access the collection. + +![](/img/components/enjin-matrixchain/8.png) + +## How to create a Token + +:::info Some deposits are required. +A single of 0.01 ENJ is required to store the token on-chain. This deposit also covers the first . +An additional of 0.01 ENJ is required for each new token holder. +::: + +Tokens are created using the `mint` extrinsic. It only contains three parameters, the `recipient`, the `collection_id` and `mint params`. The mint params is an enum with a two variants: CreateToken and Mint. + +`CreateToken` is used to create a new token and set its configuration such as cap, metadata, royalty, etc. +`Mint` is used to mint additional units to an existing token. + +### CreateToken + +This must be used the first time a token is created. The provided token id must not already exist. Some additional settings can be chosen when creating a token, such as setting a cap on the supply or giving it a royalty for the marketplace. +Some of these settings can be changed later by using the `mutate_token` extrinsic. + +To create an NFT, set the cap: `supply`/`collapsing supply` to `1`. + +#### Create Token Parameters + +- : A unique token identifier for the new . View the [TokenID Structure](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md) page for more information. +- Initial Supply: The amount of token supply to initially to the recipient account. +- Account Deposit Count: The number of accounts to reserve ENJ to, for required for future `mint`/`transfer` operations. More info in the [#Account Deposit](#token-account-deposit) section below. +- Supply Cap + - None: Infinite Supply + - Supply: Set a maximum supply amount for the token. Burned units may be re-minted. + - Collapsing: Set a maximum supply amount for the token. Burned units may not be re-minted. (i.e. burning units decreases the maximum supply) +- Token Market Behavior: (can be changed later on using the `MutateToken` extrinsic) + - None: No Royalties. + - Has Royalty: Set a percentage of ENJ royalty for each filled listing on the marketplace. + - isCurrency: Allows this new token to be used as royalty (instead of ENJ royalty) for other tokens in the collection. + **\*Note**: setting a MultiToken currency as royalty isn't implemented on-chain as of now and only acts as a placeholder. +- Listing Forbidden: Whether this token should be prevented from listing on the marketplace (can be changed later on using the `MutateToken` extrinsic) +- Freeze State: + - None: Not initially frozen. + - Permanent: Token will be frozen permanently and can never be transferred to another account. + - Temporary: Token will be frozen temporarily, can be thawed by the collection owner to allow transferring. + - Never: Token will always be transferrable and can never be frozen. +- Metadata: The parameters below are for creating a token with decimal support, like a in-game currency. + - Name: The token name. Can be set to `0x` to provide an empty name. (this name takes precedence when the token name is provided in different parameters such as `uri`/`name` attributes) + - Symbol: The token symbol to be shown in different apps. + - Decimals: The token's decimals count. + Please note, this parameter does not affect the token's behavior on-chain and is used solely for display purposes in applications. It helps apps determine how to format and present the token's total supply. + e.g. A token with `supply: 175` and `decimals: 2` should be formatted as `1.75`. +- ENJ Infusion: + - Infusion: The amount of ENJ to infuse to each unit. (More info in the [#ENJ Infusion](#enj-infusion) section below) + - Anyone Can Infuse: Whether anyone will be able to add infusion to this token, or only the collection owner. + +![](/img/components/enjin-matrixchain/9.png) + +### Mint + +This is used when minting additional units to an already existing token, as long as the circulating token supply didn't reach its cap. + +![](/img/components/enjin-matrixchain/10.png) + +## Transferring tokens and NFTs + +To perform a transfer, use the `transfer` extrinsic. There are two types of transfers: + +### Simple Transfer + +A simple transfer is when the `origin` of the extrinsic is also the sender. i.e. when the account that calling the extrinsic is also the token holder. + +![](/img/components/enjin-matrixchain/11.png) + +### Operator Transfer + +An operator transfer is when an account makes transfers on behalf of another account. This is also known as `transfer_from`. + +![](/img/components/enjin-matrixchain/12.png) + +In the example above, Alice is creating a transaction call to transfer a MultiToken to Bob's account from Charlie's account. +For the transaction to be successful, Bob must approve Alice to transfer this token in advance. + +#### Transfer Approvals + +Approvals can be set for entire collections, or they can be set for specific tokens. They can have expiration times, and specific balances can be set for token approvals. The following extrinsics are used for approvals: + +- **approve_collection -** Approves all tokens in a collection. +- **approve_token -** Approves a specific token in a collection for a specific amount. For security reasons, you must specify the exact amount of the previous approval (or zero if there is none) in the `current_amount` parameter for the extrinsic to succeed. +- **unapprove_collection -** Revokes a collection approval. +- **unapprove_token -** Revokes a token approval. + +## Burning Tokens + +Burning a refers to the act of destroying token units. Tokens can be burned by invoking the `burn` extrinsic. + +For tokens with the "Collapsing Supply" cap type, burning token units decreases the maximum supply of the token, ensuring that any burned units cannot be re-minted. + +### Melting a Token + +"Melting" is a term used to describe the process of burning a that contains an . This process not only destroys the token but also releases the infused ENJ to the token holder. +Read more in the [#ENJ Infusion](#enj-infusion) section below. +Note that "melting" is a conceptual term and does not exist as a specific function in the blockchain code. + +### Destroying Token Account + +When an account burns all of the units it owns from a specific token, its is destroyed and its is released. Read more in the [#Token Account Deposit](#token-account-deposit) section below. + +### Burning and Removing the Token from Storage + +There is an additional field on the `BurnParam` called `remove_token_storage`. If this is set to `true` and the token's circulating supply is zero, the token will be removed from the blockchain storage, effectively destroying the token from existence. This action can only be performed by the collection owner. + +When a is removed from storage, the is returned to the collection owner, and it will be as if the token never existed, so it can be recreated in the future with different configuration. + +![](/img/components/enjin-matrixchain/13.png) + +## Setting/Removing Attributes + +To add or update metadata for a token or a collection, use the `set_attribute` extrinsic. Providing `token_id` sets the attribute to the token, otherwise it sets it directly to the collection. It's only callable by the collection's owner. + +To remove an `attribute`, use `remove_attribute` extrinsic. It's only callable by the collection owner. If the `token_id` is provided, the attribute will be removed from the token. Otherwise, it will be removed from the collection. + +### Setting Collection Attributes + +This is done by clicking the `Manage Attributes` button under the expanded collection. Use the toggle to switch between setting and removing attributes. + +![](/img/components/enjin-matrixchain/14.webp) + +### Setting Token Attributes + +This is done by clicking the `Manage Attributes` button on the token page. Use the toggle to switch between setting and removing attributes. + +![](/img/components/enjin-matrixchain/15.webp) + +## Freezing + +Accounts, collections, and tokens can be frozen to prevent transfers. This is done using the `freeze` extrinsic, which expects freezing `info` to be provided. The `info` specifies whether the , , Collection Account or should be frozen. + +### Freeze a collection or a collection account + +This is done by clicking on Freeze button under expanded collection. +Use the toggle to switch between freezing a specific collection account and the whole collection. + +![](/img/components/enjin-matrixchain/16.png) + +### Freeze a token or a token account + +This is done by clicking on Freeze button on the token page. +Use the toggle to switch between freezing a specific token account and the whole token. + +![](/img/components/enjin-matrixchain/17.png) + +## Thawing + +To unfreeze either collection, token, collection account or token account, use the thaw extrinsic. It expects the same `info` as the `freeze` extrinsic. + +### Thaw a collection or a collection account + +This is done by clicking on the `Thaw` button under expanded collection. +Use the toggle to switch between thawing a specific collection account and the whole collection. + +![](/img/components/enjin-matrixchain/18.png) + +### Thaw a token or a token account + +This is done by clicking on the `Thaw` button on the token page. +Use the toggle to switch between thawing a specific token account and the whole token. + +![](/img/components/enjin-matrixchain/19.png) + +## Batch operations + +It is also possible to perform certain operations in batch. The following operations are supported: + +### Batch Transfer + +Using `batch_transfer` you can batch [transfer](#transferring-tokens-and-nfts) operations, allowing to transfer multiple tokens from a single collection, to a list of `recipients` with different `amount` of tokens. + +![](/img/components/enjin-matrixchain/20.png) + +### Batch Mint + +Using `batch_mint` you can batch [mint](#how-to-create-a-token) operations, allowing minting new or existing tokens, each with it's own `recipient` and `amount`. + +![](/img/components/enjin-matrixchain/21.png) + +### Batch Set Attribute + +Using `batch_set_attribute` you can batch set attribute operations, allowing to set multiple attributes to a single collection / token. If `token_id` is `None`, the attribute is set on the collection. If it is `Some`, the attribute is set on the token. + +![](/img/components/enjin-matrixchain/22.png) + +### Remove All Attributes + +Removes all attributes from the given `collection_id` or `token_id`. +If `token_id` is `None`, it removes all attributes of the collection. If `token_id` is `Some`, it removes all attributes of the token. `attributeCount` must match the number of attributes set in the collection/token, or the transaction will fail. + +![](/img/components/enjin-matrixchain/23.png) + +## ENJ Infusion + +The ENJ Infusion of a token represents the backing value of each unit in ENJ, which is returned to the token holder when it's ed. + +Each token unit may have some ENJ infused into it. The token creator can choose to infuse ENJ to any of its tokens at any given time, or at the time of creation. + +![](/img/components/enjin-matrixchain/24.png) + +Infused ENJ can only be retrieved by [burning the token supply](#burning-tokens). +Burning the token releases the ENJ to the holder. + +In addition, the token creator can choose to allow anyone to add ENJ infusion to an existing token, or restrict it so only the creator can add ENJ infusion. + +![](/img/components/enjin-matrixchain/25.png) + +## Token Account Deposit + +On top of the required to store the token on-chain, a Storage Deposit of 0.01 ENJ is required to store each Token Account on-chain. +A Token Account is created when an account has a balance of 1 or more, and it is destroyed when that balance becomes zero. + +It's important to note that the storage deposit is required for each new Token Account created, not for the total supply of tokens minted. +For example: + +- Minting/transferring 1,000 of a single MultiToken to **a single recipient account that doesn’t already hold a balance of that token** will require creating a Token Account for that recipient. This process incurs a one-time storage deposit of only 0.01 ENJ, covering the entirety of the 1,000 tokens. + +- Minting/transferring any balance of a single MultiToken to **1,000 different recipient accounts, each of which doesn’t already hold a balance of that token**, will require creating a Token Account for each recipient. This process incurs a storage deposit of 0.01 ENJ per account, resulting in a total deposit of 10 ENJ. + +The token creator can reserve some ENJ for Account Deposits on token creation with the `account_deposit_count` field. +ENJ reserve for Account Deposits can be added / removed ahead of time by any account, with the `update_account_deposit` extrinsic. + +On a `mint`/`transfer` operation, if a new Account Deposit is needed and there's no account deposit in reserve, the ENJ required for the account deposit will be taken on demand from the account that performed the mint or transfer. If it is an operator transfer, it is taken from the source account. +In the future, if the created Token Account is destroyed, the ENJ used for the Account Deposit is returned to the account that originally reserved the on-demand deposit. diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/02-fuel-tank-pallet.md b/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/02-fuel-tank-pallet.md new file mode 100644 index 0000000..220ae21 --- /dev/null +++ b/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/02-fuel-tank-pallet.md @@ -0,0 +1,370 @@ +--- +title: "Fuel Tanks Pallet" +slug: "fuel-tank-pallet" +description: "Subsidize fees for your players while reducing overall costs." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +:::info The Enjin Blockchain Console +Use [console.enjin.io](https://console.enjin.io/) to use the user interface referenced in this document. +You may also check the [hands-on guide for creating and using fuel tanks](/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md) +::: + +## What is a Fuel Tank? + +Fuel Tanks are special accounts that are used purely for transaction fees. Developers can choose to subsidize costs for their customers by depositing ENJ tokens to a Fuel Tank they control. ENJ tokens deposited to a Fuel Tank cannot be withdrawn, but they are released to the tank owner upon tank destruction. + +A fuel tank can whitelist specific tokens, tags, transaction types or users that will be permitted to use it. + +The fuel tank’s ID may be specified in any transaction. The chosen fuel tank will cover transaction costs if its requirements are met. Any remaining costs will be paid by the Fee Payer. + +‍Rules are added to a fuel tank to fine-tune its permissions for specific operators, tokens, time limits and value limits. + +## Terminology + +### Dispatch + +Dispatch refers to the act of broadcasting a transaction via a Fuel Tank, which will subsidize the call's . Depending on the configuration of the Fuel Tank, it may also subsidize any required by the call. + +### Accounts + +#### Tank User Account + +Some fuel tanks require users to register an account to use them. The owner of the fuel tank can specify whether only they can add accounts or if users are allowed to create their own accounts, with or without specific requirements, with the `User Account Management` field. +Each fuel tank account requires a 0.01 ENJ deposit for the Tank User Account + +#### User Account Management + +By default, only the Fuel Tank owner has the permission to add accounts to the Fuel Tank User Accounts. However, the Fuel Tank can be configured to allow accounts to add themselves by setting the `userAccountManagement.tankReservesAccountCreationDeposit` field. + +- **Default Behavior**: If the `userAccountManagement` field is not configured, only the Fuel Tank owner can add accounts to the Fuel Tank User Accounts. +- **Self-Addition without Deposit Funding**: If the `userAccountManagement.tankReservesAccountCreationDeposit` field is set to `false`, the Fuel Tank allows accounts to add themselves to the Fuel Tank User Accounts. However, the Fuel Tank will not provide the funds required for the Tank User Account . +- **Self-Addition with Deposit Funding**: If the `userAccountManagement.tankReservesAccountCreationDeposit` field is set to `true`, the Fuel Tank allows accounts to add themselves to the Fuel Tank User Accounts, and it also covers the necessary funds required for the Tank User Account . + +### Rules + +#### Account Rules + +Account Rules are rules that are validated at the time of Tank User Account Creation. + +#### Dispatch Rules + +Dispatch Rules are rules that are validated at the time of dispatch. +When a dispatch call is made to a fuel tank, it must be made in accordance with a set of rules. These rules, known as rule sets, can include multiple individual rules that determine the validity of the call. A fuel tank can have multiple rule sets, each of which controls access and permissions to the fuel tank's functionality and resources. + +#### Available Rules + +##### Whitelisted Callers: + +Whitelisted callers are accounts that are explicitly granted permission to make calls to a fuel tank and/or create Tank User Accounts. Any calls made by accounts not on the whitelist will be rejected and fail. + +##### Whitelisted Collections: + +Whitelisted collections refer to a list of specific collections allowed to be called by dispatch on a fuel tank. If a dispatch call involves a collection that is not within the whitelisted collections, it will be rejected and fail. This ensures that calls are only made to the specific collections authorized to be accessed. + +##### Max Fuel Burn Per Transaction: + +It is a setting that limits the amount of fuel that can be consumed in a single transaction. It is a safety measure that helps prevent misuse and overuse of the fuel tank's resources by ensuring that a single transaction doesn't consume too much fuel, which is important to ensure a sustainable network. + +##### User Fuel Budget: + +A user fuel budget is the total amount of fuel allocated to a specific user. It sets a limit on the amount of fuel that can be consumed by a user's transactions. Once the user's fuel budget is exhausted, their transactions will fail until the reset period is passed / their account data is removed / more fuel is added to the budget. This is meant to avoid the overuse of resources and ensure a sustainable network. + +##### Tank Fuel Budget: + +A tank fuel budget is the total amount of fuel allocated to a specific rule set. It sets a limit on the amount of fuel that can be consumed by all transactions made through that rule set. Once the tank's fuel budget is exhausted, all transactions will fail until the reset period is passed / more fuel is added to the tank's budget or overall consumption is lowered. + +##### Require Token: + +The "Require token" setting determines that a specific token must be held by the caller for the call to be accepted by the fuel tank. If the caller does not possess the required token, the call will be rejected and fail. This feature is intended to ensure that only users who hold the specified token can access the fuel tank's functions and resources. + +##### Require Signature: + +The "Require Signature" setting determines that a call must be signed by a specific account, and this signature must be included in the dispatch settings for the call to be accepted by the fuel tank. +If the signature is not provided, is invalid or is signed by a different account, the call will be rejected and fail. This feature is particularly useful with [managed wallets](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) to allow multiple controlled accounts to use the fuel tank without paying s in advance. + +##### Whitelisted Pallets: + +Whitelisted Pallets refer to a list of specific pallet names that are allowed to be executed using this rule set. This setting only considers the pallet name, not the extrinsics or parameters passed to it. Any call that includes pallets not on the list will be rejected and fail. This ensures that only certain pallets are authorized for calls made by the users, providing a finer level of control over what actions are allowed. + +##### Permitted Extrinsics: + +Permitted extrinsics refer to a list of specific extrinsic function names that are allowed to be executed using this rule set. This setting only considers the extrinsic name, not the parameters passed to it. Any call that includes extrinsics not on the list will be rejected and fail. This ensures that only certain extrinsics are authorized for calls made by the users, providing a finer level of control over what actions are allowed. + +##### Permitted Calls: + +Permitted calls refer to a list of specific extrinsic calls that are allowed to be made using this rule set, including both the extrinsic name and the specific parameters. Any calls made to other extrinsics, or extrinsic calls with non-permitted arguments, will be rejected and fail. This ensures that only authorized extrinsic calls with specific arguments can be made by the users, providing a finer level of control over what actions are allowed. + +### General Tank Terms + +#### Coverage Policy: + +This determines the scope of the fuel tank's subsidy. +Setting the Coverage Policy to "FEES" means it will subsidize only . +Alternatively, setting it to "FEES_AND_DEPOSIT" ensures it covers both and any the dispatched call may require. + +#### Freezing: + +"Freezing" is a state where a fuel tank or a rule set is temporarily prevented from accepting calls. This means that while a fuel tank or rule set is frozen, no dispatches are allowed to occur on it. This is implemented as a safety measure to prevent accidental or malicious changes and to ensure that the fuel tank or rule set remains in a stable state until the freeze is lifted. +Also note that in order to mutate any fuel tank or rule set configurations, it must be frozen first. + +#### Descriptor: + +A descriptor is a list of all the data needed to create a fuel tank. It includes details such as the fuel tank's name, account management rules, rule sets and other configuration information related to the fuel tank. The descriptor acts as a blueprint that defines how the fuel tank is set up and how it will function. It contains all the information needed to create and configure the fuel tank, and can be used to modify the fuel tank's settings. + +#### Require Account: + +The "Require Account" setting determines whether the caller must have a tank account in order to dispatch. +If "Require Account" is set to true, the caller must have a tank account; otherwise, their dispatch will be rejected and fail. +If "Require Account" is set to false, the caller can dispatch even without a tank account. +It is important to note that when "Require Account" is set to false, anyone will be able to dispatch, which could quickly drain the tank funds. Therefore, it is crucial to implement strict dispatch rules when using "Require Account: false" to prevent misuse. + +### Extrinsics + +#### create_fuel_tank: + +Create a new fuel tank by providing a descriptor. Extrinsic generates a discrete `AccountId` for the fuel tank based on passed in parameters, it takes a storage deposit and emits `FuelTankCreated` event in the success case. Creation of already existing fuel tank will result in `FuelTankAlreadyExists` error, while duplicate rule kinds within a rule set will result in `DuplicateRuleKinds` + +#### mutate_fuel_tank: + +Applies provided mutation to the fuel tank. Caller must be the owner of the fuel tank, otherwise call will result in `NoPermission` error. Tank must be frozen in order to mutate it, otherwise call will result in `RequiresFrozenTankOrRuleset` error. Duplicate rule kinds within a rule set will result in `DuplicateRuleKinds`. In success case emits `FuelTankMutated` event + +#### add_account: + +Adds a new fuel tank user account, which not only allows using fuel tank, but also stores user consumption and rule set data. An account can be created only if account rules are successfully validated. An account is required to dispatch calls when the ruleset's `require_account` is set to `true`. +A storage deposit is required, and may be paid by the user or the fuel tank, depending on the settings. Could fail with `NoPermission` if caller is not the owner and user management settings don't allow users to create accounts on their own. Creation of already existing account withing a tank will result in `AccountAlreadyExists` error. In case some of account rules cannot validate the caller, rule specific error will be returned. In success case emits `AccountAdded` event + +#### remove_account: + +Removes a user account from a fuel tank and returns the storage deposits. Fuel tank must be frozen for operation to succeed, otherwise call will result in `RequiresFrozenTank` error. Account must not contain any rule data, otherwise call will result in `AccountContainsRuleData` error. In success case emits `AccountRemoved` event + +#### remove_account_rule_data: + +Remove account rule data if it exists. Requires the fuel tank or the rule set to be frozen, otherwise will result in `RequiresFrozenTankOrRuleset` error. Only callable by the fuel tank's owner, otherwise will result in `NoPermission` error. In success case emits `AccountRuleDataRemoved` event + +#### dispatch: + +Dispatch a call through the fuel tank, where the tank would pay for transaction fees and, if configured, provide a storage deposit. +Additional settings can be provided with the `Settings` argument such as `provide remaining fee`, or include a signature required for the `RequireSignature` dispatch rule. +All calls are subject to rule set evaluation and would result in rule specific errors in case of failure. In case the inner call fails, the `DispatchFailed` event will be emitted with wrapped dispatch error inside. In success case emits `Dispatched` event. + +#### dispatch_and_touch: + +Same as dispatch, but also does create an account for the caller with `add_account` operation if it doesn't exist. Fuel tank's `user_account_management` settings must allow self-service account creation, otherwise call will result in `NoPermission` error. + +#### schedule_mutate_freeze_state: + +Schedule mutating of fuel tank or rule set `is_frozen` state for `on_finalize` to execute with help of `process_freeze_queue` helper function. Additional 1 read and 1 write are added to account for `on_finalize` storage operations. Only callable by the fuel tank's owner, otherwise will result in `NoPermission` error. In case queue already has maximum number of items, will result in `FreezeQueueFull` error. In success case emits `MutateFreezeStateScheduled` event + +#### insert_rule_set: + +Insert a new rule set into a fuel tank. It can be a new rule set or it can replace an existing one. If it is replacing a rule set, a rule that is storing data on any accounts cannot be removed and will result in `CannotRemoveRuleThatIsStoringAccountData` error. Use `remove_account_rule_data` to remove the data first. If a rule is being replaced, it will be mutated with the new parameters, and it will maintain any persistent data it already has. Fuel tank or rule set must be frozen, otherwise will result in `RequiresFrozenTankOrRuleset` error. If fuel tank already has maximum number of rule sets, will result in `MaxRuleSetsExceeded` error. Duplicate rule kinds within a rule set will result in `DuplicateRuleKinds`. In success case emits `RuleSetInserted` event + +#### remove_rule_set: + +Remove rule set from a fuel tank. A rule that is storing data on any accounts cannot be removed and will result in `CannotRemoveRuleThatIsStoringAccountData` error. Use `remove_account_rule_data` to remove the data first. This is only callable by the fuel tank's owner, otherwise will result in `NoPermission` error. Fuel tank or rule set must be frozen, otherwise will result in `RequiresFrozenTankOrRuleset` error. In success case emits `RuleSetRemoved` event + +#### batch_add_account: + +Same as `add_account` but takes a list of `AccountIds` to add into a fuel tank + +#### batch_remove_account: + +Same as `remove_account` but takes a list of `AccountIds` to remove from a fuel tank + +#### force_set_consumption: + +Force set the fuel tank consumption. If `user_id` is `Some`, it sets the consumption for that account. If it is `None`, it sets the consumption on the fuel tank directly. Currently only callable by fuel tank owner and sudo account, otherwise will result in `NoPermission` error + +#### destroy_fuel_tank: + +Destroy the fuel tank. Only callable by owner, otherwise will result in `NoPermission` error. The fuel tank must be frozen, otherwise will result in `DestroyUnfrozenTank` error. Can only be destroyed if all accounts are removed, otherwise will result in `DestroyWithExistingAccounts` error. Returns the storage deposit and remaining balance to the fuel tank owner. In success case emits `FuelTankDestroyed` event + +## How to create a fuel tank? + +A fuel tank can be created by anyone using the `create_fuel_tank` extrinsic function, once the fuel tank is created, the owner can add funds to it and subsidize transactions that are permitted by the fuel tank. The primary function of a fuel tank is to allow a user to interact with Enjin Matrixchain without paying transaction fees (called gas fees in Ethereum). + +Create a new fuel tank by providing a `descriptor`. The extrinsic generates a discrete `AccountId` for the fuel tank based on passed in parameters, it takes a storage deposit and emits `FuelTankCreated` event in the success case. Creation of already existing fuel tank will result in `FuelTankAlreadyExists` error, while duplicate rule kinds within a rule set will result in `DuplicateRuleKinds`. + +To create a new fuel tank using the [explorer](https://console.enjin.io/#/explorer), head to `Network` → `Fuel tanks` → `Create Fuel Tank`. This page allows you to create a new fuel tank by specifying the following options: + +- **Name:** Name for the fuel tank. +- **[User Account Management](#user-account-management):** (Optional) Allows setting the permission level to add new accounts to the fuel tank. +- **[Coverage Policy](#coverage-policy):** \ → Specify the scope of the fuel tank's subsidy. +- **[Account Rules](#account-rules):** (Optional) Allows specifying rules that are evaluated before an account is added to the fuel tank. +- **[Dispatch Rules](#dispatch-rules):** (Optional) Allows specifying rules that are evaluated before an extrinsic is dispatched via the fuel tank. + +:::warning +Even though the account rules and dispatch rules are optional, it is recommended to have some rules setup for your fuel tank, a fuel tank without any rules might be open to anyone without limits. +::: + +This image shows how to create a fuel tank, with some basic rules, in the following fuel tank configuration Alice creates a fuel tank that provides storage deposits for operations that require it. The tank consists of a single ruleset that doesn't require a Tank User Account to dispatch calls, and only Bob and Charlie can dispatch calls via this ruleset. + +![](/img/components/enjin-matrixchain/26.png) + +## How to modify a fuel tank? + +To mutate or modify a fuel tank, the caller must be the owner of the fuel tank, otherwise call will result in `NoPermission` error. Tank must be frozen in order to mutate it, otherwise call will result in `RequiresFrozenTankOrRuleset` error. Duplicate rule kinds within a rule set will result in `DuplicateRuleKinds`. In success case emits `FuelTankMutated` event + +To mutate a fuel tank using the explorer, head to `Network` → `Fuel tanks`. Mutating a fuel tank involves three steps 1. Freeze the fuel tank 2. Mutate the fuel tank 3. Unfreeze the fuel tank + +1. To Freeze the fuel tank, head to `Network` → `Fuel tanks`, this will present a list of all fuel tanks, select the fuel tank you wish to mutate and select freeze/unfreeze. In the next dialog box, set `is_frozen` to `true` and execute the transaction +2. To mutate the fuel tank, head to `Network` → `Fuel Tanks` → under specific fuel tank → `Mutate`In the modal, Select the Account (the owner of the fuel tank) and select the fuel tank id (you can get this from the fuel tanks page in the previous step) and add any modifications to the fuel tank. + If successful, you should see the event with you changes in the explorer `Network` tab +3. To unfreeze the fuel tank, head to `Network` → `Fuel tanks`, and perform the same step but select the `is_frozen` to `false` + +## Add an account to a fuel tank + +Adds a new fuel tank user account. An account can be created only if [account rules](#account-rules) are successfully validated. An account may be required to dispatch calls. A storage deposit is required, and may be paid by the user or the fuel tank, depending on the settings. Could fail with `NoPermission` if caller is not the owner and user management settings don't allow users to create accounts on their own. Creation of already existing account withing a tank will result in `AccountAlreadyExists` error. In case some of account rules cannot validate the caller, rule specific error will be returned. In success case emits `AccountAdded` event + +To add an account to a fuel tank using the explorer, head to `Network` → `Fuel tanks`: + +- Select the fuel tank and click add account +- Select account to be added. +- On success you should see an `AccountAdded` event + +## Remove an account from fuel tank + +Removes a user account from a fuel tank and returns the storage deposits. Fuel tank must be frozen for operation to succeed, otherwise call will result in `RequiresFrozenTank` error. Account must not contain any rule data, otherwise call will result in `AccountContainsRuleData` error. In success case emits `AccountRemoved` event + +To remove an account from a fuel tank using the explorer: + +1. Head to `Network` → `Fuel tanks`, and follow similar instructions to remove an account. +2. Select the fuel tank and click remove account + +![](/img/components/enjin-matrixchain/27.png) + +## Remove an account rule data + +As part of enforcing the budget and max spending limits of the fuel tank, the fuel tank stores data for every account. The `remove_account_rule_data` allows to remove a user account rule data if it exists. Requires the fuel tank or the rule set to be frozen, otherwise will result in `RequiresFrozenTankOrRuleset` error. Only callable by the fuel tank's owner, otherwise will result in `NoPermission` error. In success case emits `AccountRuleDataRemoved` event. + +:::warning +Similar to the process of mutating a Fuel Tank, the Fuel Tank must be frozen for the operation to succeed, otherwise the call will result in an error. +::: + +To remove an account rule data, head to `Developer` → `Extrinsic` → `fuelTanks` → `removeAccountRuleData`, and select the `tankId`, `accountId` to remove data from and the rule set and rule type to remove data from. + +![](/img/components/enjin-matrixchain/28.png) + +## Dispatch a transaction using a fuel tank + +Dispatch a call through the fuel tank, where the tank would pay for transaction fees and, if configured, provide a storage deposit. All calls are subject to rule set evaluation and would result in rule specific errors in case of failure. In case the inner call fails, the `DispatchFailed` event will be emitted with wrapped dispatch error inside. In success case emits `Dispatched` event. + +To dispatch a transaction using a fuel tank, head to Network → Fuel tanks, select the fuel tank you wish to use and select Dispatch. + +Inside the dialog box that opens, select: + +- The extrinsic you want to call via the fuel tank. +- The rule set id you wish to apply to the transaction. +- The extrinsic to execute. +- The dispatch settings if necessary. + +![](/img/components/enjin-matrixchain/29.png) + +On success, you should see the `CallDispatched` event. + +## Selecting a fuel tank to dispatch with + +The `fuelTanks_selectFuelTank` API method is a part of the matrix runtime, designed to allow users to select a fuel tank from a list of given fuel tanks. Here are step-by-step instructions on how to call this method, including the necessary parameters and expected output. + +### Method Definition + +```rust +#[method(name = "fuelTanks_selectFuelTank")] +fn select_fuel_tank( + &self, + caller: Address, + call: Vec, + settings: Vec, + fuel_tanks: Vec
, + at: Option, +) -> RpcResult>; +``` + +### Parameters + +- `[Required]` caller (Address): The address of the caller who is invoking the method. +- `[Required]` call (`Vec`): A vector of bytes representing the call that is to be made. +- `[Required]` settings (`Vec`): A vector of bytes representing the settings to be applied during the call. This should match the settings provided during the actual call or the selction will not work correctly. +- `[Required]` fuel_tanks (`Vec
`): A vector of addresses representing the available fuel tanks to select from. If no fuel tanks are provided then all fuel tanks are searched. +- at (Option\>`, which can be interpreted as: + +- `Option<(Address, u32)>` : An optional tuple where: + `Address`: The selected fuel tank's address. + `u32`: The amount of fuel (funds) available in the selected tank. + +If no appropriate fuel tanks are found the rpc will return null. + +### How it works + +The provided RPC method `fuelTanks_selectFuelTank` works by internally selecting the most suitable fuel tank for a transaction based on the caller's request and the set of predefined rules. The evaluation process considers various factors such as the fee required to execute the transaction, whether the fuel tank is frozen, and if the caller's account is associated with the fuel tank. The goal is to find the fuel tank that can cover the transaction costs with the least expense to the user while complying with all applicable rules. + +It is important to note that this RPC method relies heavily on the proper configuration and the correct availability of fuel tanks. If the settings passed during the selectFuelTank phase and actual call differs then the result might be unreliable. + +### Example + +- PolkadotJS + +```javascript +import { ApiPromise, Keyring, WsProvider } from '@polkadot/api'; + +const MATRIX_URL = "ws://127.0.0.1:56287"; + +async function sendTransaction() { + // Initialize the provider + const provider = new WsProvider(MATRIX_URL); + + // Create the API instance + const api = await ApiPromise.create({ + provider, + rpc: { + fuelTanks: { + selectFuelTank: { + params: [ + { name: 'callerAddress', type: 'AccountId' }, + { name: 'call', type: 'Vec' }, + { name: 'settings', type: 'Vec' }, + { name: 'fuelTanks', type: 'Vec' }, + { name: 'at', type: 'Hash', isOptional: true } + ], + }, + } + } + }); + + // Create a keyring instance and add Alice's account + const keyring = new Keyring({ type: 'sr25519' }); + const alice = keyring.addFromUri('//Alice'); + + // Define recipient and amount for the transfer + const recipient = 'cxMefrbkBrS5CQNTsZNxrntTMnakzVLnR9d5f3q5TuhDBynex'; + const amount = 1000000000000; + + // Create the transfer call + const transferCall = api.tx.balances.transferKeepAlive(recipient, amount); + + // Debug: Log the transfer call + console.log('Transfer call (hex):', transferCall.toHex()); + + // Define settings and fuelTanks with proper types + const settings = new Uint8Array(); // or the actual settings if available + const fuelTanks = []; // Pass an empty array if no specific fuel tanks are provided + + // Perform the RPC call to select the fuel tank + const response = await api.rpc.fuelTanks.selectFuelTank( + "cxMefrbkBrS5CQNTsZNxrntTMnakzVLnR9d5f3q5TuhDBynex", + transferCall.tou8a(), + settings, + fuelTanks, + null // Use 'null' for optional 'at' parameter if not required + ); + + // Debug: Log the response + console.log('Response:', response); +} +``` diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/03-enjin-matrixchain.md b/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/03-enjin-matrixchain.md new file mode 100644 index 0000000..ea7476d --- /dev/null +++ b/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/03-enjin-matrixchain.md @@ -0,0 +1,22 @@ +--- +title: "Enjin Matrixchain" +slug: "../enjin-matrixchain" +description: "Layer 1: The utility layer of the Enjin Blockchain." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +## What is the Enjin Matrixchain? + +Enjin is a blockchain platform that provides a developer-friendly Matrixchain built on the Enjin Relaychain specifically designed for digital assets. Unlike most digital assets that run on general-purpose chains, Enjin provides faster on-chain digital asset creation and interactions, greater security, interoperability, and flexibility. +Enjin's digital asset functionality is built right into the protocol level, allowing developers to focus on building their apps instead of worrying about security and functionality. Enjin's developer-friendly approach offers a simplified and streamlined experience that can accelerate the creation and adoption of digital assets on the blockchain. + +## Enjin Matrixchain Features + +![](/img/components/enjin-matrixchain/30.webp) + +The Enjin Matrixchain incorporates an array of features that enable digital asset creation, minting, transfer, on-chain marketplace actions, royalties, crafting, swapping, fuel tanks, and more. These features are integrated into the chain protocol, making them readily accessible to all users without requiring additional setup. However, if users require specific functionality not included in the core protocol, they can leverage custom smart contracts to extend the capabilities of the platform. This flexibility ensures that Enjin can accommodate a wide range of use cases, from simple transactions to complex asset interactions. + +- Asset minting: Minting assets on Enjin is highly customizable, supporting the creation of both and non-fungible () assets, with capped or uncapped supply. With these features, Enjin provides a versatile platform for creating and distributing digital assets that can meet a wide range of use cases. +- Fuel Tanks: Enjin's Fuel Tanks feature enables the subsidization of transaction fees for different actions on the platform. This feature is highly customizable, allowing users to define custom parameters and rules that suit their specific use case. By providing this flexibility, Enjin can accommodate a wide range of transaction types, from simple transfers to more complex asset interactions. +- On-Chain Marketplace: The On-Chain Marketplace offers marketplace functionality at the protocol level, which allows for the trading of any item without requiring token listings or even their existence on the network. This feature enhances price discovery by enabling bids on non-listed tokens. Additionally, the marketplace functionality on the protocol level guarantees that royalties will be credited for all asset sales, regardless of the marketplace on which the sale occurred, without requiring custom smart contracts. diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/03-marketplace-pallet.md b/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/03-marketplace-pallet.md new file mode 100644 index 0000000..c4d5701 --- /dev/null +++ b/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/03-marketplace-pallet.md @@ -0,0 +1,151 @@ +--- +title: "Marketplace Pallet" +slug: "marketplace-pallet" +description: "Create secure peer-to-peer economies." +--- +:::info The Enjin Blockchain Console +Use [console.enjin.io](https://console.enjin.io/) to use the user interface referenced in this document. +::: + +## Overview + +The marketplace is used for trading assets on `pallet-multi-tokens`. + +## Terminology + +### Listing + +Represents an asset being put up for sale. It contains additional information that determines the behavior of the sale. + +### Auction + +A listing with an end time that accepts bids. + +### Offer + +A listing that is a request to buy an asset. + +### Royalty + +A percentage of a sale is given to an account when an asset is sold. + +### Salt + +Bytes used to differentiate listings that have the same values. + +### Make Asset + +The asset being sold. +When creating an offer, it's the asset being requested. + +### Take Asset + +The asset requested. +When creating an offer, it's the asset being bought. + +### Fee side + +The side that is charged the protocol fee (the make or take side). + +### Currency + +This concept is used by the marketplace to determine what assets are allowed to be listed, and which side of the listing is charged fees. + +## Config Values + +- `maxRoundingError: Balance = 1000` + - The max amount that can be lost due to rounding before failing. +- `maxSaltLength: u32 = 32` + - Max length of salt used in listings and bids +- `minimumBidIncreasePercentage: Perbill = 5%` + - The percentage of the minimum bid in an auction must increase by. +- `listingActiveDelay: BlockNumber = 5` + - The minimum number of blocks that must pass before a listing is active. + +## Types + +- `ListingId = H256 ([u8;32])` +- AssetId - a `CollectionId` and `TokenId` + +See pallet-multi-tokens for additional types, including `CollectionId`, `TokenId`, and `Balance`. + +## Listings + +Listings are created with the `create_listing` extrinsic. There are three types of listings: + +- Fixed Price listings allow assets to be purchased for a specific price and have no time limit. Assets are purchased using the `fill_lisitng` extrinsic. Partial fills are supported. +- Timed Auctions operate like an auction. The listing is created with an initial bid price and a time limit. Bids are placed with the `place_bid` extrinsic and must start at the initial price or more, and then each bid must increase by a fixed percentage (currently 5%). When the time limit is over, the `finalize_auction` must be called to officially end the auction and transfer the funds. +- Offer listings are created by the buyer. Seller may accept the offer using the `fill_lisitng` extrinsic. The seller and the buyer may counter offer using the `placeCounterOffer` extrinsic, in which case they can accept / reject / counter offer using the `AnswerCounterOffer` extrinsic. + +## Protocol Fee + +A protocol fee (currently 2.5%) is taken from all sales. + +## Royalties + +Royalties are currently limited to between 0.1% and 50%. If a royalty is changed after an asset is listed and the seller would get less than they expect, the sale does not go through. This is stored in the `minimum_take_value` field of the listing. + +A collection can only allow certain currencies to be accepted as royalties by adding them to its `explicit_royalty_currencies` field. If a listing is created that will pay royalties in an unaccepted currency, the listing creation will fail. + +## Fee Side + +The fee side determines which side of the listing, the make side or take side, will be charged the protocol fee. The royalty is determined by the opposite side of the fee side, and it is paid in the currency of the fee side. + +The fee side is determined with the following checks: + +1. If the make side is a currency, it pays the fee. +2. If the make side is not a currency, but the take side is a currency, the take side pays the fee. +3. If neither side is a currency, there is no fee. However, this is not currently allowed, and the listing cannot be created. + +## State + +Stores information about the marketplace. + +`listings` + +- Key: `ListingId` - The id of the listing +- Value: Listing +- Stores all listings. + +`listingIdsByMakeAsset` + +- Key: (`CollectionId`, `TokenId`) - The collection id and token id of the make asset. +- Value: `ListingId `- The id of the listing +- Allows querying listings by make asset. + +`listingIdsByTakeAsset` + +- Key: (`CollectionId`, `TokenId`) - The collection id and token id of the take asset. +- Value: `ListingId `- The id of the listing +- Allows querying listings by take asset. + +`listingIdsByAccountId` + +- Key: `AccountId `- The account id of the seller +- Value: `ListingId `- The id of the listing +- Allows querying listings by the seller. + +## Extrinsics + +`create_listing` - Places a sell / auction / offer order. + +The listing does not become active until after the `listingActiveDelay` (5 blocks) has passed. If it's an auction, it starts when its `start_block` is reached. + +A Token cannot be listed on the marketplace if its listing_forbidden field is set to true. + +**`cancel_listing`** - Cancels the listing. Only callable by the seller. + +**`fill_listing`** - Fills a fixed price listing. This will execute immediately. + +**`place_bid`** - Places a bid on a listing. The listing must be an auction, and it must be currently active. An auction is considered active if the current block is between the start and end blocks of the auction. + +Only the latest bid is stored on an auction. All bids must increase by the `minimumBidIncreasePercentage`, and the first bid must be higher than the price set when the listing was created. + +**`finalize_auction`** - This will end the auction and transfer funds. It fails if the auction is not over. It can be called by anyone. + +**`place_counter_offer`** - Creates a Counter offer to an existing offer listing. Can only be called by an account that owns the token. + +**`answer_counter_offer`** - Either accept, reject, or counter a counter offer. Callable by the offer creator (buyer) and the counter offer creator (seller). +When a counter offer is successfully accepted/rejected, it is removed from storage. + +**`set_protocol_fee`** - Change the protocol fee. Can only be called by the `ProtocolFeeOrigin` set in the config. diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/_category_.json b/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/_category_.json new file mode 100644 index 0000000..2e2a91c --- /dev/null +++ b/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Enjin Matrixchain" +} + \ No newline at end of file diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/04-enjin-blockchain.md b/versioned_docs/version-2.x/04-enjin-blockchain/04-enjin-blockchain.md new file mode 100644 index 0000000..6a17264 --- /dev/null +++ b/versioned_docs/version-2.x/04-enjin-blockchain/04-enjin-blockchain.md @@ -0,0 +1,26 @@ +--- +title: "Enjin Blockchain infrastructure" +slug: "infrastructure" +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +## What is Enjin Blockchain? + +The Enjin Blockchain is a Layer 1 protocol designed with native NFT functionalities embedded within the protocol layer. The network is scalable and efficient because it is governed by a delegated Proof-of-Stake () consensus protocol. At the time of writing the Enjin is able to process over 5,000 transactions per second (). + +This documentation provides technical details and guidance for developers aiming to integrate with or leverage the Enjin Blockchain's capabilities. + +Enjin Blockchain is made up of the Enjin Relaychain and the Enjin . + +![](/img/components/blockchain-infrastructure/1.png) + +### Enjin Relaychain + +The Enjin Relaychain serves as the central backbone of the Enjin ecosystem. It acts as a hub where each can their holdings in ENJ and undergo validation processes. The Relaychain primarily focuses on a limited set of transaction types, which encompass various ways to engage with the governance mechanism. Its purpose is deliberately streamlined, purposely omitting support for smart contracts. The primary function of the Relaychain revolves around system coordination, encompassing both itself and the interconnected Matrixchains. Meanwhile, specific tasks and functionalities are delegated to the Matrixchains, each with unique implementations and features. + +In essence, a Matrixchain represents an application-specific data structure that possesses global coherence and can be validated by the validators participating in the Relaychain. These Matrixchains are specifically designed to cater to the needs of different projects, offering specialized functionalities tailored to non-fungible tokens (NFTs). This allows for customization options and isolated data storage, empowering various projects to establish their own distinctive internal economies. + +### Enjin Matrixchain + +The Enjin Matrixchain, on the other hand, stands as a decentralized, permissionless, and trustless network that provides opportunities for anyone to operate a and actively participate in the network's activities. The network's underlying architecture aims to create a scalable and secure platform dedicated to NFTs at the protocol level. Leveraging the Substrate framework, the Enjin Matrixchain endeavors to deliver a robust infrastructure that ensures the seamless functioning and growth of the NFT ecosystem. diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/_category_.json b/versioned_docs/version-2.x/04-enjin-blockchain/_category_.json new file mode 100644 index 0000000..902721c --- /dev/null +++ b/versioned_docs/version-2.x/04-enjin-blockchain/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Enjin Blockchain" +} + \ No newline at end of file diff --git a/versioned_docs/version-2.x/05-enjin-platform/02-chain-state.md b/versioned_docs/version-2.x/05-enjin-platform/02-chain-state.md new file mode 100644 index 0000000..e7101af --- /dev/null +++ b/versioned_docs/version-2.x/05-enjin-platform/02-chain-state.md @@ -0,0 +1,99 @@ +--- +title: "Reading Chain State" +slug: "reading-chain-state" +description: "Learn how to monitor and manage the chain state for your Enjin blockchain platform, ensuring optimal performance and data integrity." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +The chain state of a blockchain is a snapshot of all the data it contains at a given moment. + +This includes account balances, ownership of digital assets, and other on-chain information. + +It's regularly updated through the network's consensus algorithm to maintain data integrity. + +To query this state, there are several methods, but let's focus on using the Enjin Console and the Enjin Platform: + +## Enjin Platform + +The Enjin Platform uses a blockchain indexer to gather transactions, aiming to provide the latest chain state as quickly as possible. + +Direct queries to the blockchain via an RPC node are possible but can be demanding on resources, especially with many simultaneous queries. Indexers mitigate this issue. + +With the Enjin Platform, you can use advanced queries involving multiple relationships to get data faster than directly from RPC nodes. + +Here's an example of a query in the GraphiQL playground, showing the state of collection ID 2000: + +```graphql +query GetCollection { + GetCollection(collectionId: 2000) { + owner { + account { + address + } + } + maxTokenCount + maxTokenSupply + forceSingleMint + frozen + tokens { + totalCount + edges { + node { + tokenId + } + } + } + } +} +``` +```json +{ + "data": { + "GetCollection": { + "owner": { + "account": { + "address": "efTpCuJYg7jnjA8HxYb9dFKp7eCP7WKKHVoCjqy1Dt7NABds6" + } + }, + "maxTokenCount": null, + "maxTokenSupply": null, + "forceSingleMint": false, + "frozen": false, + "tokens": { + "totalCount": 0, + "edges": [] + } + } + } +} +``` + +:::warning Current Chain State +It's important to know that the Enjin Platform only keeps track of the current state of the chain. This means you can't check what happened in the past blocks. The platform is designed for giving you up-to-date information about the chain's current state, not for looking at its history. +::: + +## Enjin Console + +### Connecting to the Enjin Console + +Open the Enjin Console, you can connect to the network by clicking on the following URL: https://console.enjin.io/ + +![Enjin Console Chain State](/img/guides/going-open-source/enjin-console-chain-state.png) + +## Querying the data + +To look up data from a specific module (called a ) on the Chainstate page of the Enjin Console, you just need to pick the pallet you're interested in and then select the data item you need from that pallet. + +Remember that the database used by the blockchain node is structured like a tree (specifically, a radix tree), which is different from a standard SQL database. + +- **Pallet Storage** on a blockchain is like a separate database for each category of data. +- **Specific Storage** within a pallet is akin to a table in a database, holding actual data entries. + +Unlike a traditional SQL database with tables, a blockchain organizes data in a tree structure for efficiency, but the concept of separate areas for different data types (pallets) and specific datasets (storages) within those areas is similar. + +![Querying data from the Enjin Console](/img/guides/going-open-source/enjin-console-querying-data.png) + +:::tip Specifying the Block Hash +If you do not enter the block hash to query, the chain state displayed to you will always represent the latest block created in the network. +::: diff --git a/versioned_docs/version-2.x/05-enjin-platform/03-working-with-events.md b/versioned_docs/version-2.x/05-enjin-platform/03-working-with-events.md new file mode 100644 index 0000000..6b96dbf --- /dev/null +++ b/versioned_docs/version-2.x/05-enjin-platform/03-working-with-events.md @@ -0,0 +1,177 @@ +--- +title: "Working with Events" +slug: "working-with-events" +description: "Explore how to handle blockchain events within the Enjin platform, enabling real-time updates and interactions in your application." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +In the blockchain ecosystem, "events" refer to important actions or changes within the blockchain network. + +This includes things like creating or changing digital assets, transferring balances, and any other actions that alter the chain's state. These events are recorded on the blockchain, providing a transparent and unchangeable history of all activities. + +Monitoring and analyzing these events helps users and developers understand the flow of transactions and respond to specific actions or conditions in the network. This is key for decentralized applications and games, enabling automated interactions and improving overall functionality. + +:::warning Note on Address Formats in Platform Events +Please note that all account addresses included in platform events, such as channel names or any addresses within the event payload, are formatted as **public keys**. +For example, in a "Transfer" event, the "from" account address appears in its public key format. +If needed, you may use the [Account Format Transform tool](https://matrix.subscan.io/tools/format_transform) to convert between **SS58-encoded address format** (e.g., "cx...123" for the Canary Matrixchain or "cn...123" for the Canary Relaychain) and public key format. +::: + +## Using Enjin Platform + +To view events emitted from a transaction conducted on the platform, you can use the `GetTransaction` query as follows: + + + +```graphql +query GetTransaction { + GetTransaction(id: 40000) { + method + state + result + events { + edges { + node { + moduleld + eventId + params { + type + value + } + } + } + } + } +} +``` + + +```json +{ + "data": { + "GetTransaction": { + "method": "BatchTransfer", + "state": "FINALIZED", + "result": "EXTRINSIC_SUCCESS", + "events": { + "edges": [ + { + "node": { + "moduleId": "Balances", + "eventId": "Withdraw", + "params": [ + { + "type": "who", + "value": "f64fda4cb0352aff9394f0cc14495ca8cefffb8c8e9b37de058a1d4d4650c3313" + }, + { + "type": "amount", + "value": "20675071170417823" + } + ] + } + }, + { + "node": { + "moduleId": "MultiTokens", + "eventId": "CollectionAccountCreated", + "params": [ + { + "type": "collection_id", + "value": "2155" + }, + { + "type": "account", + "value": "303049fa4535f21acc5760521c6cc91f01ddf28ebf771da99effbf5c1031416F" + } + ] + } + } + ] + } + } + } +} +``` + + + +Our platform broadcasts network events through a `WebSocket` channel, similar to the recent events feature in Polkadot-JS. It's important to note that this channel includes not only network-wide events but also specific events from the Enjin Platform. + +Learn more about [Websocket Events here](/05-enjin-platform/03-working-with-events.md). + +![Platform Events](/img/guides/going-open-source/platform-events.png) + +Our platform uses WebSockets to transmit real-time events, but since WebSockets can sometimes be unstable, there's a risk of missing events if your system loses connection. To address this, we also cache events in a query called `GetPendingEvents`. The most effective way to stay updated in real time and ensure no events are missed is to always `acknowledge` the events you receive. + + + +```graphql +query GetPendingEvents { + GetPendingEvents( + acknowledgeEvents: false, + first: 50 + ) { + edges { + node { + name + data + } + } + } +} +``` + + +```json +{ + "data": { + "GetPendingEvents": { + "edges": [ + { + "node": { + "name": "platform:transaction-updated", + "data": { + "id": 179419, + "state": "PROCESSING", + "method": "BatchTransfer", + "result": null, + "transactionId": null, + "idempotencyKey": "4eace652-9c9f-4721-bfe3-1be6f7597c32", + "transactionHash": null + } + } + }, + { + "node": { + "name": "platform:transaction-updated", + "data": { + "id": 179417, + "state": "PROCESSING", + "method": "BatchTransfer", + "result": null, + "transactionId": null, + "idempotencyKey": "a3e62297-983e-4a14-81a9-975976302127", + "transactionHash": null + } + } + } + ] + } + } +} +``` + + + +## Using Enjin Console + +In the explorer tab, you can find the recent events emitted in the network. Those will appear as soon as they are emitted. + +![Viewing events on Enjin Console](/img/guides/going-open-source/enjin-console-events.png) + +You can also see the events emitted in a single block by querying the block directly + +![Viewing events on Enjin Console](/img/guides/going-open-source/enjin-console-events-2.png) diff --git a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/02-local-installation.md b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/02-local-installation.md new file mode 100644 index 0000000..0612873 --- /dev/null +++ b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/02-local-installation.md @@ -0,0 +1,187 @@ +--- +title: "Local Installation" +slug: "local-installation" +description: "Learn how to install Enjin's blockchain tools locally on your system, enabling you to develop and test your applications offline or in a controlled environment." +--- +## Running it locally + +The [Enterprise On-Prem Enjin Platform](https://github.com/enjin/platform) was made to be as simple as possible to run, you will be able to have a full running environment in less than 5 minutes. + +### 1\. Install Dependencies + +We have two dependencies that must be installed in your server or computer, git, a command line version control system, you can download it here: https://git-scm.com/downloads + +And docker, you may install either the [docker engine](https://docs.docker.com/engine/install/) or [docker desktop](https://www.docker.com/products/docker-desktop), though for people that have never used docker before we recommend using docker desktop: https://www.docker.com/products/docker-desktop + +### 2\. Clone the Repository + +In order for this to work you have to clone the repository, please note that downloading it as a zip will generate problems with the submodules. + +To clone the repository just run the following command in the Terminal or Command Prompt in the folder where you want to download the platform. + +```bash +git clone https://github.com/enjin/platform +``` + +:::info Logging in +You login by editing the `configs/core/.env file`. Here, you use the `DAEMON_ACCOUNT` and `BASIC_AUTH_TOKEN` environment variables for the wallet account and API token, respectively. +::: + +### 3\. Run the Start Script + +In the main folder of the platform, there are two start scripts: `start.bat` for Windows and `start.sh` for macOS and Linux. + +Running either script sets everything up automatically. + +#### Running the Start script on Windows + +To run the Start script on windows, simply double click the `start.bat` file. + +#### Running the Start script on MacOS / Linux + +To run the Start script on MacOS / Linux, open your Terminal application on MacOS, or establish an SSH connection to your Linux server. +Then, navigate to the platform's root directory using the `cd platform` command, and start the script using the following command: + +```bash +./start.sh +``` + +*** + +A sample output is available to show what happens during this process. + +```text +Welcome to Enjin Platform, this script will help you start it up +For a few things to work, we need to know your platform URL +Please input the URL: (e.g. http://127.0.0.1) +http://127.0.0.1 + +Laravel uses an app key to protect your data with encryption +Your APP_KEY is not set, do you want to generate one? (y/n) +y + +Done, your key is: base64:CGAdVfzKICzydRuipAxt9UxlbfMjggDiiT52T7P9H0A= +We also use a static token to protect your platform from unauthorized access +Your BASIC_AUTH_TOKEN is not set, do you want to generate one? (y/n) +y + +Done, your basic static token is: 01f093ba63b4285b0ec451f406e3d8580f28ec32b97ac7e97f3e4bed31b44907 +Finally, we also use a password to protect your wallet daemon +Your KEY_PASS is not set, do you want to generate one? (y/n) +y + +Done, your daemon password is: 0c0b44096a6f54d3ab92aff019a7dd5ecf383fd0df908e71aec34b62145e96e5 + +[+] Building 11.8s (26/26) FINISHED +............ + +Let's get your wallet daemon address, please wait... +[+] Running 1/1 + ✔ Container platform-daemon-1 Started + +Your wallet daemon address is: efUGPaFNbV3JyqEfv27eRWkQ2tcMd5ympi83p1c2q5VUXPttv +Do you want to start all platform services? (y/n) +y + +[+] Building 179.1s (141/167) +............... + +[+] Running 8/8 + ✔ Container platform-decoder-1 Started + ✔ Container platform-redis-1 Running + ✔ Container platform-daemon-1 Running + ✔ Container platform-database-1 Running + ✔ Container platform-websocket-1 Started + ✔ Container platform-app-1 Started + ✔ Container platform-ingest-1 Started + ✔ Container platform-beam-1 Started + +Your Enjin Platform is now running, please visit: http://localhost:8000/graphiql +``` + +### 4\. Choose Your Network + +For the Enjin Platform Starter, the default network is set to `enjin-matrixchain` (mainnet). You can switch different networks by editing NETWORK variable in the `configs/core/.env` file. +Accepted values are `enjin-matrixchain` / `canary-matrixchain` / `local-matrixchain`. + +:::info To apply the changes, run without caching +When starting the Enjin Platform using the start script, it builds the Docker image with caching. If you've modified the `.env` variables, rebuild using `docker compose build --no-cache` to apply these changes. +::: + +### 5\. Access the Platform + +There are two ways of interacting with Enjin Platform, through the GraphQL API. For testing you can use the GraphiQL playground that will be accessible at http://localhost:8000/graphiql + +![Local Platform GraphiQL Playground](/img/guides/going-open-source/local-platform-graphiql.png) + +But you can also use the Enjin Platform UI which you can access at `http://localhost:8000`, you might see this setup screen where you should enter the `BASIC_AUTH_TOKEN` that was shown to you in the setup script. + +![Setup Configuration](/img/guides/going-open-source/setup-configuration.png) + +![Platform User Interface](/img/guides/going-open-source/platform-ui.png) + +After that, you will be able to navigate through the UI and create collections, tokens, fuel tanks, and everything that the Enjin Platform provides to you. + +### 6\. Make the platform publicly accessible + +Creating a public URL using ngrok + +**Step 1:** Download ngrok from the official website: https://ngrok.com/download + +**Step 2:** Unzip to Install + +On Linux, or macOS you can unzip ngrok from a terminal with the following command. On Windows, just click on ngrok.zip to extract it. + +`$unzip /path/to/ngrok.zip` + +**Step 3:** Connect your account + +Running this command will add your auth token to the default ngrok.yml configuration file. This will grant you access to more features and longer session time. + +`$ngrok config add-authtoken ONE_AUTH_TOKEN_HERE` + +**Step 4:** Running ngrok + +Read the documentation here, Try it out by running it from the command line: + +`$ ngrok help` + +To start an HTTP tunnel forwarding to your local port 8000, run this next: + +`$ ngrok http 8000` + +Once you have run the command from above, the example should appear as the following, where the URL shown is the URL of your project. + +In case you want to access the GraphQL playground, you would just need to add /graphiql at the end of your HTTP + +### 7\. Upgrade to latest version + +**Step 1:** **Update the submodules** by navigating to each of the `platform-decoder` and `wallet-daemon` folders and run: + +```bash +git checkout master && git pull +``` + +**Step 2:** **Rebuild the docker images** with no-cache from the root of the platform: + +```bash +docker compose build --no-cache +``` + +**Step 3:** **Run the container** by running this code from the root of the platform: + +```bash +docker compose up -d +``` + +### 8\. Configure Collections (Optional) + +By default, the Enjin Platform syncs token data from all of the collections in the Blockchain. +To save storage and sync time, you can configure the Enjin Platform to sync only selected collections. +To do that, head over to http://localhost:8000/graphiql and run the `AddToTracked` mutation, with the IDs of the collections you wish to sync, like so: + +```graphql +mutation SyncCollections{ + AddToTracked(type: COLLECTION, chainIds: ["30710", "42100"]) +} +``` diff --git a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/03-faq.md b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/03-faq.md new file mode 100644 index 0000000..2d8d763 --- /dev/null +++ b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/03-faq.md @@ -0,0 +1,33 @@ +--- +title: "FAQ" +slug: "faq" +description: "Find answers to frequently asked questions about the Enjin platform, including setup, integration, and troubleshooting tips." +--- +## Q: How often is the Enjin Platform Updated? + +Updates are scheduled monthly, specifically on the second Tuesday of each month. However, updates may be rolled out sooner if there's a need to support new features, blockchain upgrades, or to address critical security issues. + +## Q: Does the Enjin Platform update automatically? + +No, the Enjin Platform doesn't have an 'automatic' update feature. Instead, the starter project is set to use the stable release branches, which are typically updated manually once a month, on the second Tuesday of the month. Users must update their projects to the latest stable version as it becomes available. + +## Q: What type of data does the database store, and do I need to back it up? + +The database holds both cached data from the blockchain and off-chain data such as Beams. It's advisable to back up your data regularly. Note that tables like `transactions`, `wallets`, `events`, and `pending_events` contain essential data not on the blockchain and should always be backed up. Other tables are mostly caches and can be reconstructed from blockchain data. Also ensure to back up your wallet daemon's seed, password, and file name to recover your setup fully. + +## Q: How do I disable the included wallet daemon and use an external one? + +To disable the internal daemon, remove its configuration from the `docker-compose.yml` file. This will prevent it from starting up with the other services, allowing you to use an external daemon that only permits outgoing connections for enhanced security. Here's a snippet of what you might remove: + +```yaml +daemon: + image: enjin/daemon:latest + build: + context: . + dockerfile: configs/daemon/Dockerfile + restart: unless-stopped + volumes: + - ./configs/daemon/store:/opt/app/storage +``` + +By excluding this section, the internal daemon won't be initialized, and you can then set up an external wallet daemon as needed. diff --git a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/04-enterprise-on-prem.md b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/04-enterprise-on-prem.md new file mode 100644 index 0000000..aa3c93a --- /dev/null +++ b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/04-enterprise-on-prem.md @@ -0,0 +1,87 @@ +--- +title: "Enterprise On-Prem" +slug: "../enterprise-on-prem" +description: "Find out how to set up enterprise on-prem for your blockchain infrastructure, giving you control over your environment and data security." +--- +:::info Choosing the right platform for you +There are two version of the Enjin Platform +- The [Enjin Platform Cloud](http://platform.enjin.io/) + - A cloud-hosted version that you can start using straight away. + - Ideal if you're new to Enjin or don't want to manage your own infrastructure. +- The [ Enterprise On-Prem Enjin Platform](https://github.com/enjin/platform) + - A self-hosted solution that offers customization and full control over the software. + - Best for those who are more tech-savvy. It gives you full control over the data and source code, allowing you to customize the platform extensively, including adding new features, writing your own Beam conditions or creating custom events. However, running it requires systems administration skills. You'll need to handle backups, updates, server maintenance, and set up internet access and SSL certificates for the platform. +::: + +## Advantages of Running the Enjin Platform On-Premises (Enterprise) + +Enterprise On-Prem offers significant benefits for app development: + +- **Seamless Transactions**: Includes transaction retries, wallet nonce management, and gas estimation for smooth operations. +- **Comprehensive Web3 Tools**: Offers a blockchain indexer, GraphQL API, and other vital infrastructure for efficient blockchain interaction. +- **Reliable Server Performance**: Features a production-grade HTTP server, ensuring robust app hosting. +- **Scalability and Flexibility**: Highly scalable, supports millions of users, and fully customizable to meet specific needs. +- **Efficient Data Handling**: Fast blockchain data retrieval with advanced filtering, and real-time updates through subscriptions to events. +- **Automated Processes**: Supports automated wallet management and asset transfers, streamlining user interactions. +- **Enhanced Data Integrity**: Includes quick sync and auto-healing features for maintaining up-to-date and accurate database information. + +## Real-time Platform Architecture + +The package consists of three main components: + +### Indexer + +Our package includes a blockchain indexer that quickly retrieves blockchain data, speeding up the process compared to direct chain queries. It also allows for creating relationships and filtering data in ways not possible with direct queries. + +### APIs + +Our GraphQL API offers a server-based interface for a variety of queries and mutations, enabling easy access to information and transaction execution on the blockchain. + +### Events + +The Enjin Platform allows developers to subscribe to events for real-time updates on blockchain activities, reducing the need for constant API checks. This simplifies blockchain interactions by eliminating complex tasks like encoding and transaction signing. + +## Understandig the API and Indexer + +The Enjin Platform uses an indexer for fast blockchain data retrieval, bypassing slower direct queries. It employs GraphQL as its main API, allowing developers to efficiently access only the data they need, which enhances performance and reduces bandwidth. + +Enabled Automation + +- Request for user proof; +- Authentication request; +- Creating managed wallets; +- Wallet linking and switching; +- Asset transfer and management; +- Enjin Beam support; +- Fuel Tanks support; +- Marketplace support. + +The Enjin Platform indexer is designed to minimize the blockchain's storage and database growth. Indexers organize data, making it easier for developers to retrieve the necessary information. + +### Data Management Tool + +The Data Management Tool from Indexers organizes and structures blockchain data. + +It creates a centralized database, making it easier for game developers to access specific data they need without sorting through all blockchain data. + +### Quick Sync Method + +The Enjin Platform features a quick sync method allowing developers to rapidly update and store the latest blockchain state using a pre-packaged index. + +This method saves time by avoiding full blockchain syncing. + +A "Worker" service manages and processes all blockchain blocks and their associated events. + +### Auto-Healing Feature + +The Enjin Platform has an auto-healing feature that constantly checks the database's health. + +If it finds problems like corruption or mismatches, it automatically updates to match the current blockchain state, keeping data accurate and current. + +### Events & WebSockets + +The Enjin Platform sends notifications through WebSocket channels about activities on the platform. + +For instance, it alerts when a new collection is created. Clients can get these updates by subscribing their wallets to the WebSocket channel via the Enjin Wallet Daemon. + +This daemon not only checks for transactions to sign but also listens for these event notifications, enabling real-time interaction and dynamic experiences on the platform. \ No newline at end of file diff --git a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/04-laravel-horizon.md b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/04-laravel-horizon.md new file mode 100644 index 0000000..db91192 --- /dev/null +++ b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/04-laravel-horizon.md @@ -0,0 +1,18 @@ +--- +title: "Laravel Horizon" +slug: "laravel-horizon" +description: "Discover how to use Laravel Horizon to manage and monitor queue workloads for the Enjin platform, ensuring smooth operations." +--- +The Enjin Platform's new architecture has improved how data is handled, including retrieval, processing, and storage. + +Key updates include: + +- **Processor Container**: A new container decodes information and stores it in your local database. + +- **Monitoring with Laravel Horizon**: Laravel Horizon is integrated for monitoring this processor container. It helps in tracking the status of jobs and pinpointing any failures. + +- **Dashboard Access**: To view the processor container's activities, visit the Laravel Horizon dashboard at: http://localhost:8000/horizon/dashboard. + +For more details on Laravel Horizon, refer to their documentation: https://laravel.com/docs/10.x/horizon. + +![Laravel Horizon Dashboard](/img/guides/going-open-source/laravel-dashboard.png) diff --git a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/05-extending-the-platform.md b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/05-extending-the-platform.md new file mode 100644 index 0000000..573a537 --- /dev/null +++ b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/05-extending-the-platform.md @@ -0,0 +1,88 @@ +--- +title: "Extending the Platform" +slug: "extending-the-platform" +description: "Learn how to extend the Enjin platform by adding custom features and integrations to meet the unique needs of your blockchain projects." +--- +Our platform lets you add your own code to customize it without changing the original code. For example, you can use the `adhocRules` feature in the Beam Mutation to set custom rules. + +One way to use this is by adding a `ValidationRule` to the `ClaimBeamMutation`. This rule limits how many times a user can claim a Beam, based on their IP address and wallet account. It ensures a user can only claim once per IP or account. + +When a Beam is claimed, we save the user's IP and wallet account in the database. Using this, we can check if a claim has already been made from that IP or account. If it has, the claim is denied. + +First, we create a Laravel `ValidationRule`. Then, we add code to check the `beam_claims` table for the user's IP or account. If it's already there, the claim fails and an error message is shown. This check happens early in the process, stopping claims before they go too far. + +```php +data = $data; + + return $this; + } + + public function validate(string $attribute, mixed $value, Closure $fail): void + { + $code = Arr::get($this->data, 'code'); + $cap = 1; + + if (BeamClaim::hasCode($code) + ->where( + fn ($builder) => $builder + ->where('wallet_public_key', $value) + ->orWhere('ip_address', request()->getClientIp()) + ) + ->count() >= $cap) { + + $fail($this->message()); + } + } +} +``` + +In the code, we use a fixed limit (`$cap`) of 1 for claims, but you can make this adjustable by using an environment variable (ENV). This avoids the need for new builds when changing the cap. + +After creating the validation rule, add it to the `ClaimBeamMutation` by placing it in the `adhocRules` array. Do this in the `boot()` method of your app's `AppServiceProvider` class. This setup ensures the rule checks the supplied `account` field and its data. + +```php +public function boot() +{ + ClaimBeamMutation::$adhocRules = ['account' => [new ClaimBeamPerUser()]]; +} +``` + +This example shows how to add custom rules for extra checks in the Beam package without modifying its core code. You can also include more checks beyond just the user's IP address and wallet account if needed. diff --git a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/_Telemetry_02-local-installation.md b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/_Telemetry_02-local-installation.md new file mode 100644 index 0000000..4775f91 --- /dev/null +++ b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/_Telemetry_02-local-installation.md @@ -0,0 +1,197 @@ +--- +title: "Local Installation" +slug: "local-installation" +description: "Learn how to install Enjin's blockchain tools locally on your system, enabling you to develop and test your applications offline or in a controlled environment." +--- +## Running it locally + +The [Enterprise On-Prem Enjin Platform](https://github.com/enjin/platform) was made to be as simple as possible to run, you will be able to have a full running environment in less than 5 minutes. + +### 1\. Install Dependencies + +We have two dependencies that must be installed in your server or computer, git, a command line version control system, you can download it here: https://git-scm.com/downloads + +And docker, you may install either the [docker engine](https://docs.docker.com/engine/install/) or [docker desktop](https://www.docker.com/products/docker-desktop), though for people that have never used docker before we recommend using docker desktop: https://www.docker.com/products/docker-desktop + +### 2\. Clone the Repository + +In order for this to work you have to clone the repository, please note that downloading it as a zip will generate problems with the submodules. + +To clone the repository just run the following command in the Terminal or Command Prompt in the folder where you want to download the platform. + +```bash +git clone https://github.com/enjin/platform +``` + +:::info Logging in +You login by editing the `configs/core/.env file`. Here, you use the `DAEMON_ACCOUNT` and `BASIC_AUTH_TOKEN` environment variables for the wallet account and API token, respectively. +::: + +### 3\. Run the Start Script + +In the main folder of the platform, there are two start scripts: `start.bat` for Windows and `start.sh` for macOS and Linux. + +Running either script sets everything up automatically. + +#### Running the Start script on Windows + +To run the Start script on windows, simply double click the `start.bat` file. + +#### Running the Start script on MacOS / Linux + +To run the Start script on MacOS / Linux, open your Terminal application on MacOS, or establish an SSH connection to your Linux server. +Then, navigate to the platform's root directory using the `cd platform` command, and start the script using the following command: + +```bash +./start.sh +``` + +*** + +A sample output is available to show what happens during this process. + +```text +Welcome to Enjin Platform, this script will help you start it up +For a few things to work, we need to know your platform URL +Please input the URL: (e.g. http://127.0.0.1) +http://127.0.0.1 + +Laravel uses an app key to protect your data with encryption +Your APP_KEY is not set, do you want to generate one? (y/n) +y + +Done, your key is: base64:CGAdVfzKICzydRuipAxt9UxlbfMjggDiiT52T7P9H0A= +We also use a static token to protect your platform from unauthorized access +Your BASIC_AUTH_TOKEN is not set, do you want to generate one? (y/n) +y + +Done, your basic static token is: 01f093ba63b4285b0ec451f406e3d8580f28ec32b97ac7e97f3e4bed31b44907 +Finally, we also use a password to protect your wallet daemon +Your KEY_PASS is not set, do you want to generate one? (y/n) +y + +Done, your daemon password is: 0c0b44096a6f54d3ab92aff019a7dd5ecf383fd0df908e71aec34b62145e96e5 + +[+] Building 11.8s (26/26) FINISHED +............ + +Let's get your wallet daemon address, please wait... +[+] Running 1/1 + ✔ Container platform-daemon-1 Started + +Your wallet daemon address is: efUGPaFNbV3JyqEfv27eRWkQ2tcMd5ympi83p1c2q5VUXPttv +Do you want to start all platform services? (y/n) +y + +[+] Building 179.1s (141/167) +............... + +[+] Running 8/8 + ✔ Container platform-decoder-1 Started + ✔ Container platform-redis-1 Running + ✔ Container platform-daemon-1 Running + ✔ Container platform-database-1 Running + ✔ Container platform-websocket-1 Started + ✔ Container platform-app-1 Started + ✔ Container platform-ingest-1 Started + ✔ Container platform-beam-1 Started + +Your Enjin Platform is now running, please visit: http://localhost:8000/graphiql +``` + +### 4\. Choose Your Network + +For the Enjin Platform Starter, the default network is set to `enjin-matrixchain` (mainnet). You can switch different networks by editing NETWORK variable in the `configs/core/.env` file. +Accepted values are `enjin-matrixchain` / `canary-matrixchain` / `local-matrixchain`. + +:::info To apply the changes, run without caching +When starting the Enjin Platform using the start script, it builds the Docker image with caching. If you've modified the `.env` variables, rebuild using `docker compose build --no-cache` to apply these changes. +::: + +### 5\. Access the Platform + +There are two ways of interacting with Enjin Platform, through the GraphQL API. For testing you can use the GraphiQL playground that will be accessible at http://localhost:8000/graphiql + +![Local Platform GraphiQL Playground](/img/guides/going-open-source/local-platform-graphiql.png) + +But you can also use the Enjin Platform UI which you can access at `http://localhost:8000`, you might see this setup screen where you should enter the `BASIC_AUTH_TOKEN` that was shown to you in the setup script. + +![Setup Configuration](/img/guides/going-open-source/setup-configuration.png) + +![Platform User Interface](/img/guides/going-open-source/platform-ui.png) + +After that, you will be able to navigate through the UI and create collections, tokens, fuel tanks, and everything that the Enjin Platform provides to you. + +### 6\. Make the platform publicly accessible + +Creating a public URL using ngrok + +**Step 1:** Download ngrok from the official website: https://ngrok.com/download + +**Step 2:** Unzip to Install + +On Linux, or macOS you can unzip ngrok from a terminal with the following command. On Windows, just click on ngrok.zip to extract it. + +`$unzip /path/to/ngrok.zip` + +**Step 3:** Connect your account + +Running this command will add your auth token to the default ngrok.yml configuration file. This will grant you access to more features and longer session time. + +`$ngrok config add-authtoken ONE_AUTH_TOKEN_HERE` + +**Step 4:** Running ngrok + +Read the documentation here, Try it out by running it from the command line: + +`$ ngrok help` + +To start an HTTP tunnel forwarding to your local port 8000, run this next: + +`$ ngrok http 8000` + +Once you have run the command from above, the example should appear as the following, where the URL shown is the URL of your project. + +In case you want to access the GraphQL playground, you would just need to add /graphiql at the end of your HTTP + +### 7\. Upgrade to latest version + +** Step 1:** **Update the submodules** by navigating to each of the `platform-decoder` and `wallet-daemon` folders and run: + +```bash +git checkout master && git pull +``` + +**Step 2:** **Rebuild the docker images** with no-cache from the root of the platform: + +```bash +docker compose build --no-cache +``` + +**Step 3:** **Run the container** by running this code from the root of the platform: + +```bash +docker compose up -d +``` + +### 8\. Configure Collections (Optional) + +By default, the Enjin Platform syncs token data from all of the collections in the Blockchain. +To save storage and sync time, you can configure the Enjin Platform to sync only selected collections. +To do that, head over to http://localhost:8000/graphiql and run the `AddToTracked` mutation, with the IDs of the collections you wish to sync, like so: + +```graphql +mutation SyncCollections{ + AddToTracked(type: COLLECTION, chainIds: ["30710", "42100"]) +} +``` + +### 9\. Configure Telemetry (Optional) + +The Enjin Platform includes a telemetry feature that periodically collects and provides data about your platform instance. This data includes information such as package versions, host URL, configured blockchain, operating system, PHP version, and more. By default, this feature is enabled. If you prefer to disable telemetry, set the `TELEMETRY_ENABLED` environment variable to `false`. + +Additionally, you can choose to provide an email address to receive important notifications. These notifications may alert you to breaking changes, incompatible package versions, or other critical updates related to your platform configuration. To enable this, set the `UPGRADE_NOTIFICATION_EMAIL` environment variable to your preferred email address. You can remove your email address at any time by setting the `UPGRADE_NOTIFICATION_EMAIL` environment variable to `null`. + +:::note +Your email address will only be collected if the `TELEMETRY_ENABLED` environment variable is set to `true`. +::: \ No newline at end of file diff --git a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/_category_.json b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/_category_.json new file mode 100644 index 0000000..353520f --- /dev/null +++ b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Enterprise On-Prem" +} \ No newline at end of file diff --git a/versioned_docs/version-2.x/05-enjin-platform/05-enjin-platform.md b/versioned_docs/version-2.x/05-enjin-platform/05-enjin-platform.md new file mode 100644 index 0000000..9f88720 --- /dev/null +++ b/versioned_docs/version-2.x/05-enjin-platform/05-enjin-platform.md @@ -0,0 +1,107 @@ +--- +title: "Enjin Platform" +slug: "../enjin-platform" +description: "Discover the Enjin Platform, a comprehensive suite of tools for building, managing, and integrating blockchain-based assets in your projects." +--- +:::info There are three versions of the Enjin Platform to choose from: +- The [Enjin Platform Cloud (Testnet)](https://platform.canary.enjin.io/login), connected to Enjin Canary Testnet +- The [Enjin Platform Cloud (Mainnet)](http://platform.enjin.io/), connected to Enjin Blockchain Mainnet +- The [Enterprise On-Prem Enjin Platform](https://github.com/enjin/platform) where you can choose to connect to either network. +::: + +## What is the Enjin Platform? + +The Enjin Platform is the world’s most powerful and advanced open-source framework for building NFT platforms and integrations. It allows developers and their projects to easily communicate with the Enjin Blockchain, forming a light-weight and modular layer between the blockchain, and their applications. + +It's the bridge between the complex world of blockchain and user-friendly applications, minimizing the technical barriers usually associated with blockchain integration. This is achieved through a lightweight, modular layer that uses a modern, flexible GraphQL API. This layer manages all blockchain interactions, thus removing the necessity of engaging with traditional smart contracts directly. + +Consider the Enjin Platform as your digital interpreter, simplifying your communication with the blockchain. It enables you to create collections, mint and transfer blockchain tokens, and even administer blockchain collections and wallets with ease. + +The Enjin Platform works much like a customizable building set. It has a modular design, which means you can add or take away pieces as needed. These pieces come in the form of optional add-on packages. They provide extra tools, such as managing Fuel Tanks, handling marketplace listings (through the marketplace package), or generating Enjin Beams via the API (with the Enjin Beam package). In other words, with the Enjin Platform, you get a customized setup that fits your specific needs. + +This versatile platform offers two versions to cater to different needs. The [Enterprise On-Prem](https://github.com/enjin/platform), self-hosted version provides you with comprehensive control over your data, eliminating dependence on any third-party servers or services. Enjin also offers a hosted version, the Enjin Platform Cloud ([mainnet ](https://platform.enjin.io/)\|[ canary](https://platform.canary.enjin.io/)), tailored for users seeking rapid integration. You can choose the version that best fits your requirements. + +![](/img/components/4.png) + +## How does it work? + +How does Enjin Platform bring all these functionalities together? The answer lies in its structure. The platform comprises distinct components each designed to offer a unique solution for developers and creators. + +

+ +

+ +### API & Schema + +The [Open-Source Enjin Platform](https://github.com/enjin/platform) also provides a ready-made application that simplifies blockchain actions for developers by removing the need for encoding, signing transactions, and understanding complex technical details. + +The project has two key components: + +- **Enjin API:** Our GraphQL API provides a server-based interface that allows you to execute a range of queries and mutations. In other words, you can easily retrieve necessary information or execute transactions on the blockchain. +- **Events:** With the Enjin Platform, developers can subscribe to events and receive real-time updates on any on-chain activities, eliminating the need to constantly poll the API for changes. + +### Wallet Daemon + +The [Wallet Daemon](https://github.com/enjin/wallet-daemon) is the component responsible for automatically signing and broadcasting transactions to the Enjin Blockchain. Running on your own secure server, the Wallet Daemon removes the need for manual signing of each transaction by polling the Enjin Platform in 6-second intervals, fetching new transaction requests, signing them with your private key, and submitting them to a blockchain node. As the Wallet Daemon keeps track of the nonce locally, it can process and submit more than one transaction per block. + +### SDKs + +With the release of the Enjin Platform, we’re preparing multiple SDKs to further streamline integration into your projects. Our first release includes the open-source [C# SDK](https://github.com/enjin/platform-csharp-sdk) for connecting and interacting with the Enjin Platform. + +Want to learn more about our SDKs? Dive into our documentation [here](/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md). + +## Notable Platform Features + +The Enjin Platform is packed with features designed to make your life as a developer easier. Here's a glimpse of what's in store: + +### Enabled Automation + +- **User-validation requests:** A swift way to establish user identities +- **Authentication requests:** Seamless process to secure sessions +- **Managed wallet creation:** Forget the complex coding, create wallets with a few clicks +- **Wallet linking and switching:** Easily manage multiple wallets and switch between them +- **Asset transfer and management:** Smoothly transfer and manage your digital assets +- **Enjin Beam support:** Leverage Enjin Beams for unique offerings +- **Fuel Tanks support:** Efficiently manage your fuel tanks for subsidizing transaction fees +- **Marketplace support:** Create and manage marketplace listings with ease +- **Relaychain auto-teleport:** Any Relaychain ENJ mistakenly sent to managed wallet will be automatically teleported to Matrixchain + +### Data Management Tool + +Simplify data organization with our Data Management Tool. Provided by Indexers, it structures and organizes blockchain data by creating a centralized database from blockchain inputs. This means you, as a developer, can access specific data without sifting through the entire blockchain database. + +### Quick Sync Method + +The Enjin Platform offers a quick sync method that enables developers to fetch and store the most recent state of the blockchain in a pre-packaged index. This saves time and effort that would otherwise be spent on syncing the entire blockchain. The "Worker" service oversees and manages all the blocks produced in the blockchain and parses all extrinsic events in each block. + +### Auto-Healing Feature + +The Enjin Platform includes an auto-healing feature that continuously monitors the health of the database. If any issues arise, such as corruption or a saving mismatch, the platform automatically re-syncs with the current chain state to ensure that all data is correct and up-to-date. + +### Relaychain ENJ Auto-teleport + +The Enjin Ecosystem is composed of two distinct chains: the Enjin Relaychain, which handles Staking and Governance, and the Enjin Matrixchain, a hub for NFTs and marketplace functionalities. There can be a potential misunderstanding when users purchase ENJ on an exchange and deposit it into a Dapp's managed wallet on the Relaychain. This is because the Dapp creator anticipates the deposit on the Matrixchain, not the Relaychain. To address this, we've introduced the Auto-Teleport feature. This feature automatically teleports any Relaychain ENJ received in a managed wallet to the corresponding Matrixchain. It streamlines the process, eliminates room for error and the need for manual teleportation. + +### Modular Design + +The Enjin Platform is designed with a light, modular structure, allowing developers to custom-fit the integration depending on their requirements and design philosophy. The Platform and Platform Core provide fundamental functionality, while multiple optional packages are available to expand your integration. You can enhance the Platform's capabilities without modifying the core codebase. + +Developers may further augment their Enjin Platform configuration by integrating [Laravel Horizon](https://laravel.com/docs/10.x/horizon), translating all Platform data into a visually appealing dashboard. + +Everyone is welcome to contribute to the codebase and build upon the framework. Don’t forget to [Follow Enjin on GitHub](https://github.com/enjin)! + +### Platform User Interface + +The Enjin Platform comes with a lightweight and easy-to-use user interface. The graphical interface comes with GUI forms for creating, viewing, managing collections & NFTs. You can also extend the UI with additional components like Beams and Fuel Tanks, depending on their availability and installation. + +![](/img/components/6.png) + +The user interface is available as a [standalone application](https://github.com/enjin/platform-ui) for the Enterprise On-Prem Enjin Platform, but comes standard when using the Enjin-hosted implementation of the [Enjin Platform](https://platform.enjin.io/). + +## Enjin Platform Cloud + +The modularity and flexibility of the Enjin Platform can be daunting for first-timers - which is exactly why we’ve rolled out the Enjin Platform Cloud! + +This user-friendly version allows you to kickstart your next NFT project in just a few minutes. Ran on Enjin's robust servers, yet fully secure and isolated to your unique project instance, you can launch your NFT game with a few simple steps. Just create an account, set it up, generate your first API key, and voila! Your NFT game is operational. The [Wallet Daemon](https://github.com/enjin/wallet-daemon) on your own server securely handles the signing and broadcasting of all transactions, ensuring the utmost integrity of your project's security. + +Get started today on [mainnet](https://platform.enjin.io/register) or [canary](https://platform.canary.enjin.io/register). diff --git a/versioned_docs/version-2.x/05-enjin-platform/_category_.json b/versioned_docs/version-2.x/05-enjin-platform/_category_.json new file mode 100644 index 0000000..be030c0 --- /dev/null +++ b/versioned_docs/version-2.x/05-enjin-platform/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Enjin Platform" +} + \ No newline at end of file diff --git a/versioned_docs/version-2.x/06-enjin-products/01-enjin-tech-stack.md b/versioned_docs/version-2.x/06-enjin-products/01-enjin-tech-stack.md new file mode 100644 index 0000000..4f97b75 --- /dev/null +++ b/versioned_docs/version-2.x/06-enjin-products/01-enjin-tech-stack.md @@ -0,0 +1,31 @@ +--- +title: "Enjin Tech-Stack" +slug: "enjin-tech-stack" +description: "Explore the Enjin Ecosystem, providing innovative blockchain solutions for developers, enabling the integration of NFTs, tokens, and decentralized applications." +--- +Enjin is the ideal blockchain platform for your game or business due to its gaming roots and the powerful Relaychain infrastructure that it's built upon. The Enjin ecosystem comprises the Enjin Platform, NFT.io, Enjin Wallet 2.0, Beam, SDKs, and other tools and services that allow game and app developers to boost their revenue, gain a competitive advantage, and innovate in ways that were previously impossible. + +![](/img/components/1.webp) + +Enjin's tools and services, originally developed for gaming, are versatile and applicable to businesses of all sizes and industries. Whether seeking to create blockchain products or utilize tokenized digital assets for acquisition, retention, engagement, and monetization strategies, Enjin's expertise can be a valuable resource. +The Enjin Platform is a powerful Platform as a Service (PaaS) for blockchain technology, designed to simplify the creation and administration of blockchain games. + +Unlike traditional blockchain game development that requires building and maintaining complex infrastructure, the Enjin Platform offers a comprehensive, and adaptable set of tools and services. With Enjin's Blockchain SDKs, Platform API, and Automation services such as the wallet Daemon, game developers can revolutionize the creation of blockchain games. The Enjin Platform also incorporates Enjin Relaychain and Enjin Matrixchain, which serve as the blockchain backbone of the entire system. + +

+ +

+ +The Enjin Ecosystem provides numerous advantages, one of which is the ability to leverage innovative crowdfunding models. With this platform, you can create gaming assets such as characters, weapons, and real estate ahead of time and offer them to gamers through conventional crowdfunding platforms or a custom website powered by NFT.io. Using Beam, the world's first QR-based blockchain asset distribution service, assets can be easily shared with QR codes. Furthermore, you can facilitate user-driven value creation by enabling players to modify, craft, upgrade, build, and trade blockchain-based gaming assets, which can gain intrinsic value based on their history, stats, utility, and customization. + +Enjin's Ecosystem provides benefits beyond just cutting-edge crowdfunding models, including the establishment of user loyalty and trust through true ownership of digital gaming assets. This ownership leads to more loyal users who are more likely to make lost revenue by eliminating gray market trading, which can account for up to 40% of lost revenue. Furthermore, player churn can be reduced by ensuring gaming assets are owned by gamers and safely stored in their private Enjin Wallet, which requires approval for each transaction, thus making hacking or in-game fraud impossible. + +

+ +

+ +The Enjin Platform's versatility extends beyond gaming to a variety of other use cases. For instance, you can transform artwork into NFTs, allowing for easy display in virtual galleries or as proof of ownership for physical pieces. + +New technology allows you to produce music and convert it into a powerful token with exclusive rights. You can also tokenize physical collectibles like vintage comics, rare paintings, and real estate. + +Furthermore, you can design unique, easily tradeable, and programmable digital collectibles that convey a compelling narrative and leverage the Enjin Platform's 3D modeling support to create a fully immersive VR and AR experience with tokenized assets. diff --git a/versioned_docs/version-2.x/06-enjin-products/02-enjin-coin.md b/versioned_docs/version-2.x/06-enjin-products/02-enjin-coin.md new file mode 100644 index 0000000..f48fe73 --- /dev/null +++ b/versioned_docs/version-2.x/06-enjin-products/02-enjin-coin.md @@ -0,0 +1,45 @@ +--- +title: "Enjin Coin" +slug: "enjin-coin" +description: "Learn about Enjin Coin, the cryptocurrency powering blockchain-based assets and transactions within the Enjin Ecosystem, from games to applications." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +Enjin Coin (ENJ) is the native coin of the Enjin Blockchain, serving as the main token for performing actions and facilitating transactions on the network (staking, marketplace, governance, etc.) +It is used to pay  (also known as ), , and to add to s on the . + +Enjin can come in three different forms: + +- **:** This is the native version of Enjin Coin. You can buy it on cryptocurrency exchanges and use it for each or use it to create new s on the Enjin Blockchain . +- **cENJ:** This is the version of Enjin Coin. It's available for free from the [cENJ faucet](https://faucet.canary.enjin.io/) and can be used just like ENJ for transactions and minting s, but on the Enjin Canary Testnet. +- **sENJ:** sENJ represents staked ENJ. It's issued to you when you [stake your ENJ](/02-guides/02-blockchain/01-relaychain/04-joining-nomination-pools.md) and is destroyed when you unstake. You can your ENJ into a or . Once you've received your sENJ from staking, it can be [traded for ENJ](/04-enjin-blockchain/02-enjin-relaychain/02-stake-exchange-pallet.md). + +## ENJ Infusion + +Infusing ENJ to a is the process of locking Enjin Coin (ENJ) inside each unit as its backing value. + +Here's why it's important: + +- The holder can melt it at anytime to retrieve the embedded ENJ. This provides intrinsic value for s, bringing value besides it's market value. +- Once a is infused, the infused amount is required to be deposited for each new unit minted, making the asset more scarce. + +It’s important to note that once a is infused with ENJ, it's infused ENJ amount can increase but it can never be decreased. + +### Minimum Account Balance (Existential Deposit) + +To keep an Enjin Coin Account active on the network, the wallet must hold **at least 0.1 ENJ**. If the balance drops below this threshold, the account may be **automatically deleted** - unless the transfer uses the `keepAlive` flag set to `true`. + +:::note +This requirement only applies to Enjin Coin accounts. NFT () holdings are unaffected and remain safe even if the wallet has 0 ENJ. +::: + +When `keepAlive` is set to `true`, the transfer will fail if it would reduce the balance below 0.1 ENJ—ensuring the account stays active. If set to `false`, the transfer will go through, but the account may be deleted if the balance falls below the threshold. + +Use `keepAlive: true` to **prevent accidental deletion**, especially when transferring most or all of a wallet’s balance. + +### Token Account Deposit + +A of 0.01 ENJ is required for each holder. +This deposit is also known as . +Once s are destroyed, or 'burned', this deposit is returned to the issuer's account proportionally. diff --git a/versioned_docs/version-2.x/06-enjin-products/03-enjin-wallet.md b/versioned_docs/version-2.x/06-enjin-products/03-enjin-wallet.md new file mode 100644 index 0000000..017ac2d --- /dev/null +++ b/versioned_docs/version-2.x/06-enjin-products/03-enjin-wallet.md @@ -0,0 +1,24 @@ +--- +title: "Enjin Wallet" +slug: "enjin-wallet" +description: "Discover the Enjin Mobile Wallet, enabling users to manage blockchain assets, including tokens and NFTs, securely from their smartphones." +--- +## What is the Enjin Wallet? + +The Enjin Wallet underwent a complete overhaul, resulting in enhanced performance, stability, and reliability. It now boasts improved power, speed, and user-friendliness, making it an excellent option for those interested in a blockchain wallet tailored specifically to NFTs, such as game developers, art collectors, and gamers. + +The Enjin Wallet's updated user interface and designs ensure accessibility to users of all backgrounds, providing a secure and seamless solution for storing, trading and transferring NFTs. Additionally, the wallet offers extensive compatibility with multiple cryptocurrencies for storage purposes. + +## Enjin Wallet 2.0 Demo +
+ +
+ +Game developers can enjoy seamless integration with the Enjin Platform when using the new Enjin Wallet, as it is fully compatible with the Enjin Blockchain, our API, SDKs, and NFT.io support. This makes the Enjin Wallet an ideal digital wallet for storing, trading and transferring NFTs in a secure and seamless way. With its user-friendly interface and broad cryptocurrency compatibility, the Enjin Wallet is a valuable tool for anyone looking to engage with the Enjin ecosystem. + +The Enjin Wallet is not limited to the Enjin Blockchain and also supports a wide range of other blockchains, including BTC, LTC, ETH, BEP20, Polygon, Polkadot, KSM, Doge, ERC20, and other standards. This broad spectrum of compatibility ensures that users can store, trade, and manage a diverse range of digital assets all in one place. +The Enjin Wallet is available for both Android and iOS devices: + +Download from [Google Play](https://play.google.com/store/apps/details?id=com.enjin.mobile.wallet&hl=en&gl=US&pli=1) + +Download from the [App Store](https://apps.apple.com/us/app/enjin-nft-crypto-wallet/id1349078375) diff --git a/versioned_docs/version-2.x/06-enjin-products/04-nftio-marketplace.md b/versioned_docs/version-2.x/06-enjin-products/04-nftio-marketplace.md new file mode 100644 index 0000000..88f4272 --- /dev/null +++ b/versioned_docs/version-2.x/06-enjin-products/04-nftio-marketplace.md @@ -0,0 +1,26 @@ +--- +title: "NFT.io Marketplace" +slug: "nftio-marketplace" +description: "nft-io-marketplace" +--- +## What is NFT.io? + +[NFT.io](http://nft.io/) is the latest innovation from the Enjin team, a global NFT Marketplace fully integrated with the Enjin Blockchain. As the next-generation NFT blockchain, Enjin provides a robust and secure foundation for [NFT.io](http://nft.io/), offering a smart, user-friendly, and ad-free platform for all NFT enthusiasts. + +![](/img/components/7.png) + +At [NFT.io](http://nft.io/), you can easily explore digital art, crypto-collectibles, and other NFTs through a simple and accessible marketplace. Whether you're listing your favorite NFTs, bidding on special edition assets, or creating your own collections, NFT.io offers a seamless experience. Our vision is to empower you to discover, showcase, and trade NFTs in a way that reflects your unique passions and interests. So join us on [NFT.io](https://nft.io/) and experience the future of NFT marketplaces today. + +## Key Benefits & Features + +- **Connect using your very own wallet:** The platform is seamlessly integrated with the Enjin Wallet. + No Ads: Enjoy an ad-free experience on NFT.io. You can explore the platform without any interruption from pesky promotions, dangerous scams, or annoying ads. +- **Beginner Friendly:** NFT.io has been revamped from scratch, boasting a user-friendly interface that caters to people from diverse backgrounds and skill levels. Whether you're an experienced NFT trader or a beginner, NFT.io offers simplicity and ease of use for all. +- **Made for all NFTs:** NFT.io is designed to support all types of NFTs. Whether you're an art collector, a music enthusiast, a fan of crypto collectibles, or simply browsing, we cater to all NFT categories. +- **Responsive Design:** NFT.io has a responsive design that's optimized for mobile support, enabling you to access the NFT marketplace even when you're on the go. The user interface is designed to adjust automatically to fit the screen size of your device, from widescreen 4k resolution desktop monitors to smartphones and tablets of all sizes. +- **Instant listings and purchases:** Instantly list and purchase NFTs with just a few clicks. Transactions are completed within milliseconds, allowing for a smooth and efficient experience. +- **Auctions and Bidding:** You can easily set up auctions for your favorite NFTs or collectibles and place your bids. +- **Create Enjin Beams:** Leverage the power of Enjin's QR distribution system to create Enjin Beams. +- **Credit card/Fiat payment support:** The NFT.io marketplace offers a seamless integration of credit card and fiat payment methods, making it easy to purchase beam credits on the platform. +- **Full GIF/MP4/GLB File Support:** The NFT.io marketplace fully supports both GIF and MP4 files, allowing you to flawlessly showcase your 3D NFTs. +- **Receive Instant Notifications:** Get immediate notifications and email updates whenever your assets are listed, sold, or receive a bid, so you can stay on top of all your transactions diff --git a/versioned_docs/version-2.x/06-enjin-products/05-beam.md b/versioned_docs/version-2.x/06-enjin-products/05-beam.md new file mode 100644 index 0000000..dad3878 --- /dev/null +++ b/versioned_docs/version-2.x/06-enjin-products/05-beam.md @@ -0,0 +1,68 @@ +--- +title: "Enjin Beam" +slug: "beam" +description: "Discover how to use Enjin Beam to distribute blockchain assets through QR codes, making it easy to send and receive NFTs and tokens." +--- + +:::info Explore Our Comprehensive Beam Guide +For a detailed walkthrough of Enjin Beam's features and how to use them, check out our [in-depth guide for beams](/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md). +::: + +## What is Enjin Beam? + +Enjin Beam offers an efficient way to market, promote, and distribute blockchain assets. By leveraging QR code technology, Enjin Beam allows for the easy distribution of blockchain assets on the Enjin blockchain. This feature is available on NFT.io and can be used by anyone who registers on the marketplace. + +Moreover, game developers can access Enjin Beam through the Enjin Platform API, which is designed to automate the distribution process. On the other hand, [NFT.io](https://nft.io/) is intended for users who prefer a more hands-on approach. + +### Here's how Enjin Beam works: + +1. First, blockchain assets are created on NFT.io or through the Enjin Platform API. +2. The assets are then placed in a smart contract, acting as a "vault", using the Beam interface. This generates a unique QR code. +3. Users can scan the code with the [Enjin Wallet](https://enjin.io/products/wallet) app and receive the blockchain asset directly into their wallet. +4. If the user does not have the Enjin Wallet installed, they can still interact with the Enjin Web Wallet interface to claim their blockchain assets using Enjin Beam. + +### Send Anything; Post Anywhere + +Enjin Beam offers a flexible way to send rewards that can be customized to fit your target audience and marketing goals. Whether it's gaming assets, branded collectibles, or early access, Enjin Beam lets you personalize your rewards to meet your needs. + +Beam QR codes can be shared through various visually accessible platforms, including social media networks, websites, ads, videos, flyers, presentations, and even within apps or games. This allows you to reach a broad audience and get your rewards in front of the right people. + +**Enjin Beam enables you to:** + +1. Simplify the creation and generation of QR codes with a few straightforward steps. +2. Distribute NFTs on a large scale via a QR code, making it easy for users to scan and claim. +3. Enhance your online presence and offer a user-friendly experience that appeals to a wide range of users, including those who are not familiar with crypto or blockchain technology. + +![](/img/components/8.png) + +:::tip Master Enjin Beam +Want to unlock the full potential of your NFT distributions? Our comprehensive guide to Enjin Beam offers advanced techniques, detailed setup instructions, and strategic insights for maximizing your campaign's reach and impact. +➡️ **[Read the In-Depth Guide to Enjin Beam](/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md)** +::: + +## Types of Beams + +### Mint on Demand + +One of the significant differences between Enjin Beam and its predecessors is the `MINT_ON_DEMAND` feature. This feature eliminates the need to pre-mint all tokens for distribution, which is the default way of creating beams on Enjin Blockchain. With this feature, Beam creators can define a parameter on the mutation for tokens to be minted directly to users' wallets at the exact moment they are claimed, reducing the time people wait for the tokens to arrive in their wallets. + +![](/img/components/9.png) + +### Batch Mint & Batch Transfer + +Enjin Beam also introduces batch mint and batch transfer features, allowing multiple tokens to be minted and/or transferred in a single on-chain transaction. This reduces the overall system load during periods of high activity, and users can claim their tokens faster and at a significantly lower cost. With the ability to batch 150 mints into a single transaction, users can claim their tokens faster and at a lower cost. + +![](/img/components/10.png) + +### Integer Range Type + +The Integer Range type is another new feature that makes creating Beams more efficient. By using this feature, you can specify a sequential range of token IDs for distribution using a shorthand format. So instead of specifying all of the token IDs you want to distribute individually, we can now simply provide an integer range in this format: "[startId..endId]" e.g. ["1..10000"] you can also mix and match single IDs and ranges: ["1", "5..10", "12", "15", "20..32"] + +![](/img/components/11.png) + +### Set attributes on the go! + +Developers can now set attributes to the minted tokens as they're being minted on-chain for the first time. This feature allows for unique and computed metadata to be added to tokens during the creation process. +This interaction with the mint on-demand feature is a match made in heaven as game developers will be able to create the tokens. At the same time, the beam is being generated, and they can set the metadata and mint the tokens as the demand for new tokens increases. + +![](/img/components/12.png) diff --git a/versioned_docs/version-2.x/06-enjin-products/_category_.json b/versioned_docs/version-2.x/06-enjin-products/_category_.json new file mode 100644 index 0000000..0291738 --- /dev/null +++ b/versioned_docs/version-2.x/06-enjin-products/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Enjin Products", + "link": { + "type": "generated-index", + "description": "Delve into the core architecture and individual elements that constitute the Enjin ecosystem." + } +} + \ No newline at end of file diff --git a/versioned_sidebars/version-2.x-sidebars.json b/versioned_sidebars/version-2.x-sidebars.json new file mode 100644 index 0000000..caea0c0 --- /dev/null +++ b/versioned_sidebars/version-2.x-sidebars.json @@ -0,0 +1,8 @@ +{ + "tutorialSidebar": [ + { + "type": "autogenerated", + "dirName": "." + } + ] +} diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..a61da5c --- /dev/null +++ b/versions.json @@ -0,0 +1,3 @@ +[ + "2.x" +] From ceeb8a496dbce2854ec381081376e7941342e177 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 4 May 2026 17:54:05 +0300 Subject: [PATCH 02/67] Rewrite v3 wallet-daemon setup page for binary-first install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the prebuilt binary the primary path: download → mkdir store → .env with KEY_PASS, PLATFORM_KEY, and absolute SEED_PATH → run. Add a brief Docker section pointing at the 3.x branch for users who prefer it. Drops the v2-only Wallet Daemon Executable section, the Sentosa upgrade warning, and the config.json + per-network setup (the v3 daemon is network-agnostic and ignores config.json). Documents the v3.0.1 binary gotchas users hit in practice: SEED_PATH must be absolute (relative paths resolve against the build-time CARGO_MANIFEST_DIR), KEY_PASS must be set but may be empty, and .env values with spaces need quoting. Also fixes the now-broken #wallet-daemon-executable anchor in the Enjin Farmer setup guide. --- .../06-using-wallet-daemon.md | 205 +++++++++--------- .../01-setup-guide.md | 2 +- 2 files changed, 105 insertions(+), 102 deletions(-) diff --git a/docs/01-getting-started/06-using-wallet-daemon.md b/docs/01-getting-started/06-using-wallet-daemon.md index 49d1102..7a6328f 100644 --- a/docs/01-getting-started/06-using-wallet-daemon.md +++ b/docs/01-getting-started/06-using-wallet-daemon.md @@ -5,8 +5,6 @@ description: "Get started with the Enjin Wallet Daemon, an automated tool for ma --- import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; The Enjin Wallet Daemon provides a streamlined process for signing blockchain transactions, enabling seamless and efficient transactions between your game and the blockchain. This tool creates a persistent bridge between your game and the blockchain, ensuring a fluid gaming experience for players. @@ -34,139 +32,144 @@ The diagram above depicts the various stages involved in creating a new collecti *** -:::info There are two approaches for running the Wallet Daemon: -- Using the [Wallet Daemon Executable](#wallet-daemon-executable). -- Setting up [Wallet Daemon via Docker](#setting-up-wallet-daemon-via-docker). +## Setup + +:::note Code repository +The code repository can be found at https://github.com/enjin/wallet-daemon/ ::: -:::tip Choosing the Right Approach for Running the Wallet Daemon -There are two ways to run the Wallet Daemon, each suitable for different user profiles and use cases: -1. **Executable Approach:** This method involves running a simple executable, configuring it via a user interface, and clicking 'Run' to start it. It is the most straightforward approach and is recommended for beginners or those who are not developers and for development environments / personal use. Learn how to set it up in our [Setting up Wallet Daemon using Executable](#wallet-daemon-executable) guide. -2. **Docker Approach:** This method involves cloning a GitHub repository and modifying the configuration files. It offers more flexibility and control, making it suitable for developers and for production use. Learn how to set it up in our [Setting up Wallet Daemon using Docker](#setting-up-wallet-daemon-via-docker) guide. +It is recommended that the Enjin Wallet Daemon is installed and ran in isolation. This means running it on a dedicated server. The daemon itself is incredibly light-weight and does not require any extensive resources. -Choose the approach that best fits your technical proficiency and the needs of your project. +:::info Network-agnostic +The Wallet Daemon v3 is network-agnostic — it does not need to be configured for a specific chain. A single daemon instance signs transactions for all Enjin networks (Enjin Matrixchain, Enjin Relaychain, Canary Matrixchain, and Canary Relaychain). Each chain just maps to a different SS58 address derived from the same wallet, all of which are printed when the daemon starts. ::: -## Wallet Daemon Executable +The recommended way to run the daemon is to [download a prebuilt binary](#download-the-binary) for your operating system. If you'd rather run it inside Docker, see [Running with Docker](#running-with-docker) at the bottom of this page. -![The wallet daemon executable app](/img/getting-started/wallet-daemon-executable-welcome.png) +### Download the Binary -Setting up the Wallet Daemon Executable is a straight forward process. -Download the latest version of the Wallet Daemon from [GitHub](https://github.com/enjin/wallet-daemon-ui/releases), extract it, and run the `enjin_wallet_daemon.exe` executable file. -Follow the on-screen instructions to set it up and make sure to insert your Enjin Platform API Token from your [account settings page](https://platform.canary.enjin.io/settings). +Download the latest release for your operating system from the [GitHub releases page](https://github.com/enjin/wallet-daemon/releases). Extract the archive — you should end up with a single executable (`wallet-daemon` on Linux/macOS, `wallet-daemon.exe` on Windows). -![Configuring the Wallet Daemon Executable with Enjin Platform](/img/getting-started/daemon-exec-overview.gif) +Place it in a directory of your choosing. From here on, "the daemon directory" refers to wherever you placed the binary. -## Setting up Wallet Daemon via Docker +### Create the Seed Store -:::note Code repository -The code repository can be found at https://github.com/enjin/wallet-daemon/ -::: +Inside the daemon directory, create a `store` subdirectory. The daemon will write its encrypted wallet seed there on first launch. -It is recommended that the Enjin Wallet Daemon is installed and ran in isolation. This means running it on a dedicated server. The daemon itself is incredibly light-weight and does not require any extensive resources. +```bash +mkdir store +``` -:::tip Have Docker installed? -If you don't have Docker installed, you may install either the docker engine or docker desktop, though if you have never used docker before we recommend using docker desktop: https://www.docker.com/products/docker-desktop -::: +### Configure the Daemon -:::warning Action Required: [Sentosa Upgrade](https://enjin.io/blog/enjin-blockchain-sentosa-upgrade) -To ensure uninterrupted service and transaction signing capabilities, users must upgrade their Wallet Daemon to v2.1.1 or greater before the [Sentosa blockchain upgrade](https://enjin.io/blog/enjin-blockchain-sentosa-upgrade). +The daemon reads its configuration from environment variables. You can either create a `.env` file in the daemon directory (recommended) or pass the variables inline when starting the binary. -> Canary Testnet: The Sentosa upgrade is already live (since October 27, 2025). If you are using the Canary Wallet Daemon to sign transactions, you must update immediately to v2.1.1+ or transactions will fail. +| Variable | Required | Description | +|----------------|----------|-------------| +| `PLATFORM_KEY` | Yes | API token from the Settings page of the [Enjin Platform Cloud](https://platform.beta.enjin.io/settings). | +| `KEY_PASS` | Yes (may be empty) | Password used to encrypt the wallet seed. Immutable once chosen. The variable must be set, but the value may be empty — see warning below. | +| `SEED_PATH` | Yes | **Absolute** path to the `store` directory you created above (see warning below). | -> Mainnet: The upgrade is scheduled for December 8, 2025 (approx. 16:30 UTC, at Relaychain block 13229687 and Matrixchain block 7734855). While older versions will work until this date, it is highly recommended to upgrade to the latest version beforehand. +:::danger `KEY_PASS` is immutable +`KEY_PASS` is directly used to derive the wallet private key. Choose something unique and back it up securely — you cannot change it later without losing access to the wallet. -Instructions on upgrading the wallet-daemon to the latest version can be found [here](#updating-the-daemon-to-the-latest-version) +The variable itself must always be present (the daemon panics on startup if `KEY_PASS` is unset entirely). However, the value may be empty (`KEY_PASS=`), in which case the seed is effectively unencrypted at rest — anyone with read access to `wallet.seed` could recover the mnemonic. Only do this if your `store` directory is fully protected by filesystem permissions. ::: -To download the Enjin Daemon, clone the `enjin/wallet-daemon` repository from GitHub by running this command in the Terminal: +:::warning `SEED_PATH` must be an absolute path +When running the standalone binary, `SEED_PATH` must be an **absolute** path to your `store` directory (e.g. `/home/you/wallet-daemon/store` on Linux/macOS, or `C:/Users/you/wallet-daemon/store` on Windows). A relative value such as `store` will fail because the binary resolves relative paths against the build directory baked in at compile time, not your current working directory. + +If your path contains spaces, wrap the value in double quotes inside the `.env` file. +::: + +#### The `.env` file + +Create a `.env` file alongside the binary: ```bash -git clone https://github.com/enjin/wallet-daemon.git +KEY_PASS=your-unique-password +PLATFORM_KEY=your-platform-api-token +SEED_PATH="C:/Users/you/wallet-daemon/store" ``` -### Configure Daemon +### Start the Daemon -You will want to update the `.env` file with the `PLATFORM_KEY` (which is an API Token generated within the Settings page of the Enjin Platform Cloud). In addition to this, you'll want to specify a unique `KEY_PASS`. +From the daemon directory: -:::danger Important note -The `KEY_PASS` is immutable. It is directly used to derive the wallet private key. Choose something unique and make sure to backup this `KEY_PASS` in a secure manner. -::: +```bash +./wallet-daemon +``` -The final configuration is to update the `config.json` file to communicate with either the Enjin Blockchain (mainnet) or Canary Blockchain (testnet). You can do this by updating the node property in the JSON file. You can find an example of both networks below. +(or `.\wallet-daemon.exe` on Windows) + +On the initial launch, a 12-word mnemonic seed is generated and written to `store/wallet.seed`, encrypted with your `KEY_PASS`. The daemon prints the SS58 address for every supported chain on startup, for example: - - -``` -{ - "node": "wss://rpc.matrix.blockchain.enjin.io:443", - "relay_node": "wss://rpc.relay.blockchain.enjin.io:443", - "api": "https://platform.enjin.io/graphql", - "master_key": "store" -} -``` - - ``` -{ - "node": "wss://rpc.matrix.canary.enjin.io:443", - "relay_node": "wss://rpc.relay.canary.enjin.io:443", - "api": "https://platform.canary.enjin.io/graphql", - "master_key": "store" -} +******************* Enjin Wallet Daemon v3.0.1 ******************* +** Enjin Relaychain (SS58): enDD... +** Enjin Matrixchain (SS58): efRg... +** Canary Relaychain (SS58): cnU2... +** Canary Matrixchain (SS58): cxLx... ``` - - -### Starting the Daemon +If you ever need to import this wallet into another wallet app, the derivation path is: +`///` -Finally, you can spin up the daemon using the following command from your local `wallet-daemon` directory: +### Updating the Daemon -`docker compose up -d daemon` +Download the latest release from the [GitHub releases page](https://github.com/enjin/wallet-daemon/releases) and replace the existing executable. Your `store/` directory and `.env` configuration remain compatible across versions. -On the initial launch, a 12-words mnemonic seed will be created in the `/store` folder, with an additional encryption using the secret pass provided in the `KEY_PASS` environment variable. If you ever need to import this wallet into any wallet app, the derivation path is as follows: -`///` +### Importing an Existing Seed -### Updating the Daemon to the latest version - -To upgrade your Docker installation of the Wallet Daemon to the latest version, follow these steps from within your local `wallet-daemon` directory: - -1. **Pull the latest code from the repository:** - ```bash - git pull - ``` -2. **Stop the currently running container:** - ```bash - docker compose stop daemon - ``` -3. **Rebuild the Docker image to incorporate the updates:** - ```bash - docker compose build --no-cache - ``` -4. **Start the daemon again:** - ```bash - docker compose up -d daemon - ``` - -### Importing Wallet Daemon From Existing Seed +The daemon ships with a built-in `import` command that prompts for a 12-word mnemonic and writes it to `SEED_PATH`, encrypted with your `KEY_PASS`. :::info Wallet Daemon Encryption -Wallet Daemon may be encrypted with a password specified in the `KEY_PASS` env var located in the `.env` file. -If your existing wallet is encrypted with a password, make sure to update your `KEY_PASS` var accordingly. -If your existing wallet is not encrypted, leave the `KEY_PASS` var empty. +The seed file is encrypted with the `KEY_PASS` env var. Make sure `KEY_PASS` is set to the password you want to encrypt the seed with — this same value will be required to decrypt and use the wallet on every subsequent run. ::: -Follow the steps below to set up your wallet daemon from an existing seed: - -1. Find your wallet's public key. You can convert your wallet's account address to it's public key with the [Account Format Transform](https://matrix.subscan.io/tools/format_transform) tool. - We'll use wallet address `efRP7f5aFWWobNiNxcWGNxhY1RdRXZ4kScvwuFdD4bsBHEUZW` as an example. It's public key is `0x62c75d8f81e05794cd0b703cf07b7ea3196840eaac4e300cb968fdd266882e02`. -2. Remove the 0x from the public key from step #1. - For our example, it's `62c75d8f81e05794cd0b703cf07b7ea3196840eaac4e300cb968fdd266882e02` -3. Inside your local `wallet-daemon/store` folder, create a file with the name `73723235`. - For our example, we've created the file `/store/7372323562c75d8f81e05794cd0b703cf07b7ea3196840eaac4e300cb968fdd266882e02` -4. Inside the new file created on step #3, insert your wallet's mnemonic seed wrapped with double quotes. - Example: `"earn meat maid rotate ..."` -5. Rebuild your docker container for the changes to take effect: - `docker compose build` -6. Run the daemon again: - `docker compose up daemon` +1. Make sure your environment is configured with `KEY_PASS`, `PLATFORM_KEY`, and `SEED_PATH` (see [Configure the Daemon](#configure-the-daemon)). +2. Stop the daemon if it is currently running. +3. Run the import command. You will be prompted to type your 12-word mnemonic: + ```bash + ./wallet-daemon import + ``` + (or `.\wallet-daemon.exe import` on Windows) +4. Start the daemon again as usual. + +### Running with Docker + +If you'd prefer to run the daemon inside Docker rather than as a standalone binary, clone the `3.x` branch of the repository (the v3 development branch) and use the included [`docker-compose.yml`](https://github.com/enjin/wallet-daemon/blob/3.x/docker-compose.yml). + +```bash +git clone --branch 3.x https://github.com/enjin/wallet-daemon.git +cd wallet-daemon +``` + +Create (or edit) a `.env` file in the repository root with the following values: + +```bash +KEY_PASS=your-unique-password +PLATFORM_KEY=your-platform-api-token +SEED_PATH=/wallet/store +``` + +`SEED_PATH=/wallet/store` is the absolute path the seed-store volume is mounted at inside the container. Then start the daemon: + +```bash +docker compose up -d daemon +docker compose logs -f daemon +``` + +To update later, pull the repo, rebuild, and restart: + +```bash +git pull +docker compose build --no-cache +docker compose up -d daemon +``` + +To import an existing seed under Docker, run: + +```bash +docker compose run --rm daemon wallet import +``` diff --git a/docs/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md b/docs/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md index e6deb90..592b52c 100644 --- a/docs/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md +++ b/docs/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md @@ -68,7 +68,7 @@ Next, you'll set up your Enjin Platform account and the Wallet Daemon. ### Wallet Daemon 1. Run the Wallet Daemon executable you downloaded. -2. Follow the on-screen instructions to configure it, and enter the **API Token** you just copied from the Enjin Platform into the Settings → Canary Matrixchain input field. For a detailed guide, see the [Wallet Daemon documentation](https://docs.enjin.io/getting-started/using-wallet-daemon#wallet-daemon-executable). +2. Follow the on-screen instructions to configure it, and enter the **API Token** you just copied from the Enjin Platform into the Settings → Canary Matrixchain input field. For a detailed guide, see the [Wallet Daemon documentation](/01-getting-started/06-using-wallet-daemon.md). 3. Select the Enjin Platform Canary in the network dropdown menu, and run the Wallet Daemon. 4. Once configured and running, the Wallet Daemon UI will display a wallet address. **Copy this wallet address** for the next step. From 04749777e2416838875ae1857ba9e527d713b01c Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 6 May 2026 18:18:08 +0300 Subject: [PATCH 03/67] Rewrite v3 Using-the-Enjin-API page for unified endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v3 platform exposes a single GraphQL endpoint (`https://platform.beta.enjin.io/graphql`) instead of v2's per-feature/ per-network URLs, requires `Authorization: Bearer ` (the bare token without `Bearer` is rejected), and selects the target network and chain via `network` (ENJIN/CANARY) and `chain` (MATRIX/RELAY) enum arguments on each operation. Beam is gone for now. Patches two inbound anchors that pointed at sections that no longer exist: how-to-use-graphql.md `#overview-of-graphql-endpoints` and quick-start-guide.md `#mainnet`. Both target pages still need their own v3 rewrites — these are non-breaking link updates only. Also adds `v3-API.md` and `build-test/` to `.gitignore` for the v3 migration working notes file. --- .gitignore | 4 + .../02-quick-start-guide.md | 2 +- .../01-how-to-use-graphql.md | 2 +- .../05-using-enjin-api/05-using-enjin-api.md | 92 +++++++++++-------- 4 files changed, 61 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index 9b8d6cc..61bbbca 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +# v3 documentation migration notes (working file, not for upstream) +v3-API.md +build-test/ diff --git a/docs/01-getting-started/02-quick-start-guide.md b/docs/01-getting-started/02-quick-start-guide.md index dca7742..06dbab7 100644 --- a/docs/01-getting-started/02-quick-start-guide.md +++ b/docs/01-getting-started/02-quick-start-guide.md @@ -276,7 +276,7 @@ Congratulations! You've covered the basics of setting up your project on the Enj :::warning **Ready for Production?** Remember, this guide used the **Canary testnet** for development and testing. When you are ready to launch your application live, you will need to: 1. Set up your project on the **mainnet Enjin Platform Cloud**: https://platform.enjin.io/ -2. Update your application's API calls or SDKs to use the [**mainnet GraphQL API endpoints**](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md#mainnet) +2. Update your application's API calls or SDKs to target [**mainnet**](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md#selecting-a-network-and-chain) by passing `network: ENJIN` on each query and mutation. ::: Now you can explore more advanced integrations: diff --git a/docs/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md b/docs/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md index 39619d2..be86e82 100644 --- a/docs/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md +++ b/docs/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md @@ -106,7 +106,7 @@ Explore "[Core Operations](https://platform.canary.enjin.io/graphiql)" for core :::info GraphiQL Playground: Please be aware that the provided links below are specifically for the GraphiQL Playground, which is a graphical user interface designed for exploring and interacting with the Enjin Platform GraphQL APIs directly from a web browser. -If you wish to execute GraphQL requests programmatically from your local environment or a cloud-based application, you will need to utilize the actual GraphQL endpoint URLs. These can be found [here](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md#overview-of-graphql-endpoints) +If you wish to execute GraphQL requests programmatically from your local environment or a cloud-based application, you will need to utilize the actual GraphQL endpoint URL. It can be found [here](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md#graphql-endpoint). #### Testnet: - [Core Operations](https://platform.canary.enjin.io/graphiql) - [Marketplace](https://platform.canary.enjin.io/graphiql/marketplace) diff --git a/docs/01-getting-started/05-using-enjin-api/05-using-enjin-api.md b/docs/01-getting-started/05-using-enjin-api/05-using-enjin-api.md index 471e156..5f48e15 100644 --- a/docs/01-getting-started/05-using-enjin-api/05-using-enjin-api.md +++ b/docs/01-getting-started/05-using-enjin-api/05-using-enjin-api.md @@ -8,7 +8,7 @@ import GlossaryTerm from '@site/src/components/GlossaryTerm'; ## Introduction to the Enjin API -The Enjin API is a set of programmatic interfaces that allow developers to interact with the Enjin Platform from their own applications. Built using , it enables clients to request exactly the data they need in a flexible and efficient way. Unlike traditional RESTful APIs, which require multiple endpoints for different data requirements, GraphQL enables more efficient data retrieval through a single endpoint. This flexibility reduces the number of network requests and optimizes performance, making it particularly advantageous for applications that require efficient data handling, such as blockchain and NFT platforms. +The Enjin API is a set of programmatic interfaces that allow developers to interact with the Enjin Platform from their own applications. Built using , it enables clients to request exactly the data they need in a flexible and efficient way. Unlike traditional RESTful APIs, which require multiple endpoints for different data requirements, GraphQL enables more efficient data retrieval through a single endpoint. This flexibility reduces the number of network requests and optimizes performance, making it particularly advantageous for applications that require efficient data handling, such as blockchain and NFT platforms. :::info What you'll need: - Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) to pay for . @@ -18,72 +18,89 @@ The Enjin API is a set of programmatic interfaces that allow developers to inter :::tip New to GraphQL? If you're more familiar with REST APIs, it's important to understand that our API uses GraphQL - a flexible and powerful query language for APIs. GraphQL works quite differently from REST, and knowing how to structure your queries and handle responses is essential for successful integration.\ -We recommend reviewing our [How to Use GraphQL guide](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md) to get started quickly and effectively. +We recommend reviewing our [How to Use GraphQL guide](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md) to get started quickly and effectively. ::: + ## Authentication -Access tokens assume a pivotal role in facilitating your application's interaction with the Enjin Platform API. -These tokens serve a dual purpose: +API tokens are used to authenticate your application's requests to the Enjin Platform. They serve a dual purpose: -- They grant your application access to the platform. -- They allow for automatic request approval and signing using the Wallet Daemon. +- They grant your application access to authenticated operations (mutations). +- They allow the Wallet Daemon to fetch and sign transactions on your behalf. To make authenticated calls to the Enjin Platform: -1. login or create an account in the [Testnet Enjin Platform Cloud](https://platform.canary.enjin.io). -2. Create an API token if you haven't already. -3. When making HTTP requests to the GraphQL endpoints, include your API token in the request headers using the `Authorization` field: +1. Log in to the [Enjin Platform Cloud](https://platform.beta.enjin.io/). +2. Open your [account settings](https://platform.beta.enjin.io/settings) and create an API token if you haven't already. +3. Include the token in the `Authorization` header of your HTTP requests, prefixed with `Bearer`: ``` -"Authorization": "" +Authorization: Bearer ``` -4. _(Optional)_ To automate requests made from the Enjin Platform using a wallet daemon, you need to configure your daemon with the API token. - For more details head over to [Using the Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) +:::note +Read-only queries (e.g. `GetAccount`, `GetToken`, `GetCollection`) are publicly accessible and do not require an API token. Mutations and account-scoped queries do. +::: + +4. _(Optional)_ To automate signing of transactions you create through the API, configure a Wallet Daemon with your API token. + For more details head over to [Using the Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md). -## Endpoints & Queries +## Endpoint & Queries Every data transfer strictly adheres to the HTTP/1.1 standard, with HTTPS encryption mandatory for all endpoints to ensure secure communication. -The Enjin API primarily utilizes GraphQL, meaning you'll be sending POST HTTP requests to specific endpoints with your GraphQL queries and mutations. This allows you to interact with the Enjin blockchain in a flexible and efficient manner. +The Enjin API uses GraphQL, meaning you'll be sending POST HTTP requests to a single endpoint with your GraphQL queries and mutations. This allows you to interact with the Enjin blockchain in a flexible and efficient manner. :::tip SDKs for Streamlined Development For developers looking for a more integrated experience, Enjin also provides official **Software Development Kits (SDKs)** for various programming languages. These SDKs wrap the underlying GraphQL API, offering convenient methods and abstractions to streamline development. You can find more information and links to the available SDKs on our [Software Development Kits (SDKs) page](/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md). ::: -### Overview of GraphQL Endpoints +### GraphQL Endpoint -The Enjin API is structured around four distinct GraphQL endpoints, each designed to handle specific sets of queries and mutations. This segmentation allows for efficient and organized interactions tailored to different functional areas within the Enjin ecosystem: +Unlike previous versions of the platform, which exposed separate endpoints per feature area and per network, the Enjin Platform now serves all queries and mutations from a **single GraphQL endpoint**: -1. **Core Operations:** This endpoint handles fundamental blockchain operations such as creating collections, minting and transferring s, freezing, and burning s . It provides the essential tools for managing and interacting with s on the Enjin Blockchain. +:::note GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` +::: -2. **Marketplace:** Focused on marketplace activities, this endpoint encompasses operations like listing tokens for sale, purchasing tokens, and bidding on auctions. It facilitates seamless transactions and interactions within the Enjin Marketplace. +A single endpoint covers Core operations (collections, tokens, transfers, attributes), Marketplace, Fuel Tanks, and Nomination Pools. -3. **Beam:** The Beam endpoint manages operations related to the Beam system, which is used for distributing tokens claimable via QR codes. This feature is particularly useful for promotional activities and engaging user experiences. +### Selecting a Network and Chain -4. **Fuel Tanks:** This endpoint is dedicated to the Fuel Tank system, which allows for subsidizing transaction fees. It supports operations that manage and configure fuel tanks, enabling cost-effective transactions on the blockchain. +The Enjin Platform is chain-agnostic — the network and chain you want to operate on are passed as **arguments** on each query or mutation rather than being baked into the endpoint URL. -GraphQL queries and mutations are executed by sending POST HTTP requests to the corresponding endpoint: +Most operations accept two enum arguments: -:::note GraphQL Endpoints -### Testnet: -- **Core Operations** `https://platform.canary.enjin.io/graphql` -- **Marketplace** `https://platform.canary.enjin.io/graphql/marketplace` -- **Beam** `https://platform.canary.enjin.io/graphql/beam` -- **Fuel Tanks** `https://platform.canary.enjin.io/graphql/fuel-tanks` -### Mainnet: -- **Core Operations** `https://platform.enjin.io/graphql` -- **Marketplace** `https://platform.enjin.io/graphql/marketplace` -- **Beam** `https://platform.enjin.io/graphql/beam` -- **Fuel Tanks** `https://platform.enjin.io/graphql/fuel-tanks` -::: -Here is an example of an HTTP cURL post request to create a collection on the Canary blockchain, with the API Token provided: +| Argument | Values | Description | +|-----------|--------------------|-------------| +| `network` | `ENJIN`, `CANARY` | The network to target. `ENJIN` is mainnet, `CANARY` is the testnet. | +| `chain` | `MATRIX`, `RELAY` | Which chain on the selected network. `MATRIX` is the Matrixchain (where collections, tokens, and the marketplace live). `RELAY` is the Relaychain. | + +Both default to `ENJIN` / `MATRIX` (mainnet Matrixchain) when omitted. +```graphql +query GetEnjinMatrixBalance { + GetAccount( + network: ENJIN + chain: MATRIX + address: "efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr" + ) { + balance + } +} ``` -curl --location --request POST 'https://platform.canary.enjin.io/graphql' \ + +To run the same query on the Canary testnet, change `network: ENJIN` to `network: CANARY`. + +### Example Request + +Here is an example of an HTTP cURL request to fetch an account's balance from the Canary Matrixchain, with an API token provided: + +```bash +curl --location --request POST 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: Insert your API token here' \ --d '{"query":"mutation CreateCollection($forceCollapsingSupply: Boolean) {\r\n CreateCollection(\r\n mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"forceCollapsingSupply":false}}' +-H 'Authorization: Bearer ' \ +-d '{"query":"query { GetAccount(network: CANARY, chain: MATRIX, address: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\") { balance } }"}' ``` ## API Reference @@ -96,3 +113,4 @@ For a comprehensive guide on exploring and utilizing GraphQL queries and mutatio - New to GraphQL? Learn how to structure queries and mutations in our [How to Use GraphQL guide](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md). - To automate signing requests, continue to the [Using the Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) page. - Or, If you're ready to start building... [Create a Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md). +::: From 062baeadff4a767ab2da65828291d687633d5aa4 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 6 May 2026 18:35:20 +0300 Subject: [PATCH 04/67] Rewrite v3 How-to-Use-GraphQL page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates every example for the v3 API model: single `https://platform.beta.enjin.io/graphql` endpoint, explicit `network`/`chain` enum args, `tokenId: N` (no `{ integer: N }` wrapper), `CreateTransaction(transaction: { mintToken: ... })` in place of the direct `MintToken` mutation, and `CreateBatchTransaction` for batched extrinsics. Replaces the v2 GraphiQL playground list (8 per-feature / per-network URLs) with a single playground link. Rewrites the Pagination section from scratch. v2's GraphQL Relay-style connections (`pageInfo`, `edges`, `node`, per-edge `cursor`, `after:`) do not exist in v3 — replaced with offset pagination (`limit`/`page`, flat array) and cursor pagination (`limit`/`cursor`, `*CursorPagination` wrapper with `data`/`perPage`/`nextCursor`/ `previousCursor`). All inline images and gifs removed (no placeholders) — they all showed v2 UI flows and would need to be re-captured against v3. --- .../01-how-to-use-graphql.md | 521 ++++++++---------- 1 file changed, 233 insertions(+), 288 deletions(-) diff --git a/docs/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md b/docs/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md index be86e82..49f6e17 100644 --- a/docs/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md +++ b/docs/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md @@ -13,22 +13,21 @@ For everyone else, this page will walk you through the essentials of working wit ## Understanding GraphQL vs. RESTful API Calls -To illustrate the differences between GraphQL and RESTful API calls, let's consider an example of creating a token. +To illustrate the differences between GraphQL and RESTful API calls, let's consider an example of minting a token. ### RESTful API Example -In a typical RESTful API, you might have an endpoint like `/createToken` with a POST request that includes the necessary data in the request body. This might look something like: +In a typical RESTful API, you might have an endpoint like `/mintToken` with a POST request that includes the necessary data in the request body. This might look something like: ```http -POST https://platform.canary.enjin.io/api/createToken +POST https://platform.beta.enjin.io/api/mintToken Content-Type: application/json { - "recipient": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", - "collectionId": 2406, - "tokenId": 0, - "initialSupply": 1, - "cap": "INFINITE" + "recipient": "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y", + "collectionId": 7154, + "tokenId": 6533, + "amount": 1 } ``` @@ -37,19 +36,13 @@ And would traditionally return all of the available data related to the call, li ```json { "data": { - "CreateToken": { - "id": "14060", - "transactionId": "null", - "transactionHash": "null", - "method": "CreateToken", + "MintToken": { + "uuid": "a90ded41-4262-40a2-95c0-98255b660bf1", + "extrinsicHash": null, + "action": "MultiTokens.mint", "state": "PENDING", "encodedData": "...", - "wallet": { - "account": { - "publicKey": "...", - "address": "..." - } - } + "createdAt": "2026-05-06T10:00:00Z" } } } @@ -62,29 +55,34 @@ In contrast, GraphQL uses a single endpoint for all operations. Instead of defin In GraphQL: - `Queries` are used to retrieve data, similar to a `GET` request in REST. -- `Mutations` are used to create or modify data, similar to `POST`, `PUT`, or `DELETE` requests in REST." +- `Mutations` are used to create or modify data, similar to `POST`, `PUT`, or `DELETE` requests in REST. -Here's how the same RESTful request would like like in GraphQL, sent as an HTTP post request to the Canary Platform Cloud GraphQL endpoint https://platform.canary.enjin.io/graphql: +Here's how the same RESTful request would look in GraphQL, sent as an HTTP POST request to the Enjin Platform GraphQL endpoint at `https://platform.beta.enjin.io/graphql`: ```graphql -mutation CreateToken { - CreateToken( - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", - collectionId: 2406, - params: { - tokenId: { integer: 0 }, - initialSupply: 1, - cap: { type: INFINITE } +mutation MintToken { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + mintToken: { + recipient: "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y" + collectionId: 7154 + params: { + tokenId: 6533 + amount: 1 + } + } } ) { - id - method + uuid + action state } } ``` -With GraphQL, the client controls what data is returned by explicitly specifying the fields in the query (In this example - the `id`, `method`, and `state`). This enables fine-grained control over the response, minimizing unnecessary data transfer. +With GraphQL, the client controls what data is returned by explicitly specifying the fields in the query (in this example - the `uuid`, `action`, and `state`). This enables fine-grained control over the response, minimizing unnecessary data transfer. GraphQL's ability to query multiple resources in a single request, its strong type system, and its flexibility in querying data make it a powerful tool for interacting with the Enjin Blockchain, providing a seamless experience for developers building with NFTs. @@ -96,27 +94,12 @@ The serves three purposes: 2. **Write** operations using an intuitive interface. 3. **View** responses and test your operations before coding them. -![A breakdown of the GraphiQL Playground](/img/getting-started/graphiql-explorer-breakdown.png) - -### Step 1: Open GraphiQL Playground - -Choose between [Testnet](https://platform.canary.enjin.io/graphiql) or [Mainnet](https://platform.enjin.io/graphiql). +### Step 1: Open the GraphiQL Playground -Explore "[Core Operations](https://platform.canary.enjin.io/graphiql)" for core operations like managing tokens, wallets, users, and transactions. +Open the [Enjin Platform GraphiQL Playground](https://platform.beta.enjin.io/graphiql) in your browser. The Enjin API now serves all queries and mutations from a single endpoint, so there is one playground covering Core operations, Marketplace, Fuel Tanks, and Nomination Pools. :::info GraphiQL Playground: -Please be aware that the provided links below are specifically for the GraphiQL Playground, which is a graphical user interface designed for exploring and interacting with the Enjin Platform GraphQL APIs directly from a web browser. -If you wish to execute GraphQL requests programmatically from your local environment or a cloud-based application, you will need to utilize the actual GraphQL endpoint URL. It can be found [here](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md#graphql-endpoint). -#### Testnet: -- [Core Operations](https://platform.canary.enjin.io/graphiql) -- [Marketplace](https://platform.canary.enjin.io/graphiql/marketplace) -- [Beam](https://platform.canary.enjin.io/graphiql/beam) -- [Fuel Tanks](https://platform.canary.enjin.io/graphiql/fuel-tanks) -#### Mainnet: -- [Core Operations](https://platform.enjin.io/graphiql) -- [Marketplace](https://platform.enjin.io/graphiql/marketplace) -- [Beam](https://platform.enjin.io/graphiql/beam) -- [Fuel Tanks](https://platform.enjin.io/graphiql/fuel-tanks) +The link above opens a graphical interface for exploring and testing operations directly from a web browser. To execute GraphQL requests programmatically from your application, send POST requests to the actual GraphQL endpoint instead — see [Using the Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md#graphql-endpoint). ::: ### Step 2: Open the explorer @@ -125,37 +108,21 @@ Click the `Show GraphQL Explorer` button. Here, you can view a complete list of available queries and mutations, along with the associated fields and arguments for each of them. -

- Example of using the graphiql playground query builder -

- -### Step3: Add a query or mutation to start building your operation +### Step 3: Add a query or mutation to start building your operation Add a `query` or `mutation` into the "write" panel, and you'll see the list of available options in the explorer update to suit the operation type. -

- Example of switching to mutation in the graphiql playground query builder -

- ### Step 4: Select which operations and datapoints you require Click the required data points to automatically add them to the write column. This feature eliminates any guesswork when structuring your operations and helps you make the most of the Enjin Platform Schema. -

- Example of building a mutation in the graphiql playground query builder -

- ### Step 5: Click the play button to execute your operation To execute your operation and view the response, simply click the `Execute Query` button. -You can execute any on-chain operation here. You can even mint tokens and view wallet data on [Mainnet](https://platform.enjin.io/graphiql). - -

- Executing a query in the graphiql playground query builder -

+You can execute any on-chain operation here on either the Enjin (mainnet) or Canary (testnet) network — selected via the `network` argument on each operation. ## GraphQL Operation Structures @@ -174,8 +141,10 @@ It allows you to organize and distinguish different operations within a single r ```graphql query GetTokenInfo { #'GetTokenInfo' is the Operation Name that distinguishes this operation from others GetToken( + network: ENJIN + chain: MATRIX collectionId: 7153 - tokenId: { integer: 10 } + tokenId: 10 ) { supply } @@ -210,8 +179,10 @@ For instance, when using the `GetToken` query, you can request the `supply` fiel ```graphql query { GetToken( + network: ENJIN + chain: MATRIX collectionId: 7153 - tokenId: { integer: 10 } + tokenId: 10 ) { supply # supply is a Field that returns the token's supply } @@ -235,15 +206,17 @@ query { You can pass data into an operation by using arguments. -Most Queries and Mutations define a set of input data you can use, for example in the `GetToken` query you can pass in a `collectionId` and a `tokenId` argument. +Most Queries and Mutations define a set of input data you can use, for example in the `GetToken` query you can pass in `network`, `chain`, `collectionId`, and `tokenId` arguments. ```graphql query { GetToken( - collectionId: 7153 #collectionId is an argument defining which collection to pull data from - tokenId: { integer: 10 } #tokenId is an argument defining which token to pull data from + network: ENJIN # network is an argument selecting which Enjin network to read from (ENJIN or CANARY) + chain: MATRIX # chain is an argument selecting which chain on that network (MATRIX or RELAY) + collectionId: 7153 # collectionId is an argument defining which collection to pull data from + tokenId: 10 # tokenId is an argument defining which token to pull data from ) { supply } @@ -269,13 +242,13 @@ The API will tell you if you try to run an operation without a required argument ### Aliases -The alias, such as `TokenInfo` and `CurrentSupply` in the queries provided below, serve as a custom labels for specific fields within the operation. +The alias, such as `TokenInfo` and `CurrentSupply` in the queries provided below, serve as custom labels for specific fields within the operation. Aliases allow you to rename the field in the response, making the data more intuitive and organized, especially when dealing with multiple fields or nested queries within a GraphQL request. #### Operation Alias: -Usually used in batch operations, it allows you to provide a clear and descriptive name for each operations. +Usually used in batch operations, it allows you to provide a clear and descriptive name for each operation. You might use it to distinguish between multiple similar queries or to provide context for the purpose of the query. @@ -283,9 +256,11 @@ You might use it to distinguish between multiple similar queries or to provide c ```graphql query GetTokenInfo { - TokenInfo: GetToken( #TokenInfo is the ALIAS that labels the field + TokenInfo: GetToken( # TokenInfo is the ALIAS that labels the field + network: ENJIN + chain: MATRIX collectionId: 7153 - tokenId: { integer: 10 } + tokenId: 10 ) { supply } @@ -316,8 +291,10 @@ It allows you to rename individual fields in the response for clarity or to avoi ```graphql query GetTokenInfo { GetToken( + network: ENJIN + chain: MATRIX collectionId: 7153 - tokenId: { integer: 10 } + tokenId: 10 ) { CurrentSupply: supply } @@ -341,25 +318,28 @@ query GetTokenInfo { Various code blocks in the Enjin Documentation use variables instead of hardcoding arguments. This allows the dynamic insertion of data as you run mutations and queries. -For example, when a player picks up an item, your game needs to fetch the token information using the token's ID. To dynamically insert the token ID to the query, we should use a variable. +For example, when a player picks up an item, your game needs to fetch the token information using the token's ID. To dynamically insert the token ID into the query, we should use a variable. -That way, we will have one "template" for token fetching query, and the game will decide which token to fetch based on the player actions. +That way, we will have one "template" for the token-fetching query, and the game will decide which token to fetch based on the player's actions. -To work with variables in GraphQL, we first need to declare the `$variableName`, as the variables accepted by the operation. Our operations has two arguments - Collection ID and Token ID, so we'll name the variables `$collection_id` and `$token_id` respectively. Both arguments are of type `BigInt`, so the variable declaration will look like that: +To work with variables in GraphQL, we first need to declare the `$variableName` as the variables accepted by the operation. Our operation has two arguments — Collection ID and Token ID — so we'll name the variables `$collection_id` and `$token_id` respectively. Both arguments are of type `BigInt`, so the variable declaration will look like that: ``` -query GetTokenInfo($collection_id: BigInt! $token_id: BigInt) { +query GetTokenInfo($collection_id: BigInt! $token_id: BigInt!) { ``` :::tip -Note the exclamation mark (!) after the first variable declaration type BigInt. This represents a _non-nullable_ variable (essentially a _required field_). +Note the exclamation mark (!) after the variable type. This represents a _non-nullable_ variable (essentially a _required field_). ::: -We also need to replace the static value in the operation with the `$variableName` + +We also need to replace the static values in the operation with the `$variableName`: ``` GetToken( - collectionId: $collection_id //Collection ID 7153 was hardcoded here - tokenId: { integer: $token_id } //Token ID 10 was hardcoded here + network: ENJIN + chain: MATRIX + collectionId: $collection_id # Collection ID 7153 was hardcoded here + tokenId: $token_id # Token ID 10 was hardcoded here ) ``` @@ -377,10 +357,12 @@ Here's the final operation code: **Query:** ```graphql -query GetTokenInfo($collection_id: BigInt! $token_id: BigInt) { +query GetTokenInfo($collection_id: BigInt! $token_id: BigInt!) { GetToken( + network: ENJIN + chain: MATRIX collectionId: $collection_id - tokenId: { integer: $token_id } + tokenId: $token_id ) { supply } @@ -396,11 +378,6 @@ query GetTokenInfo($collection_id: BigInt! $token_id: BigInt) { } ``` - -

- Example of converting query args into variables -

- ### Types Types in GraphQL serve as blueprints for defining the shape and composition of the data available through the API, encompassing a collection of fields representing specific pieces of information. @@ -418,8 +395,10 @@ In this example, we're querying for information about a specific token. ```graphql query GetTokenInfo { GetToken( + network: ENJIN + chain: MATRIX collectionId: 7153 - tokenId: { integer: 10 } + tokenId: 10 ) { supply # Returns the current supply of the token } @@ -430,7 +409,7 @@ query GetTokenInfo { ```json { "data": { - "TokenInfo": { + "GetToken": { "supply": "1000" } } @@ -441,28 +420,37 @@ query GetTokenInfo { The `GetToken` query retrieves token information. -We specify the `collectionId` and `tokenId` as parameters to identify the token we want. +We specify `network`, `chain`, `collectionId`, and `tokenId` as parameters to identify which token, on which chain, we want. -The fields `tokenId` and supply indicate the data we want to receive in the response. +The `supply` field indicates the data we want to receive in the response. #### Mutation: -A `mutation` is used to modify data on the Enjin Platform. For example, you can use a mutation to update a user's profile information. +A `mutation` is used to modify state on the Enjin Platform — typically by creating an on-chain transaction. In v3, every blockchain operation is performed through the `CreateTransaction` mutation, with the specific action selected inside its `transaction` argument (`mintToken`, `createCollection`, `transferToken`, and so on). In this example, we're minting a new token. + ```graphql mutation MintToken { - MintToken( - recipient: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" - collectionId: "7154" - params: { - amount: 1 - tokenId: { integer: 6533 } + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + mintToken: { + recipient: "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y" + collectionId: 7154 + params: { + tokenId: 6533 + amount: 1 + } + } } ) { - id # Returns the transaction ID + uuid # Returns the transaction UUID + action # e.g. "MultiTokens.mint" + state } } ``` @@ -471,8 +459,10 @@ mutation MintToken { ```json { "data": { - "MintToken": { - "id": "1234" // request ID + "CreateTransaction": { + "uuid": "a90ded41-4262-40a2-95c0-98255b660bf1", + "action": "MultiTokens.mint", + "state": "PENDING" } } } @@ -480,31 +470,33 @@ mutation MintToken { -The `MintToken` mutation mints a new token. - -We provide the necessary arguments, such as `recipient`, `collectionId`, and `params`, to define the minting process. +The `CreateTransaction` mutation creates a new on-chain transaction. The `transaction` argument carries the specific action — here, `mintToken` — along with its parameters (`recipient`, `collectionId`, `params`). -The field `id` indicate the data we want to receive in the response. +The `uuid`, `action`, and `state` fields indicate the data we want to receive in the response. #### Batch Query: -A batch `query` allows you to send multiple mutation operations in a single request to reduce network overhead. +A batch `query` allows you to send multiple query operations in a single request to reduce network overhead. -Each query have its own set of parameters and return data. +Each query has its own set of parameters and return data. ```graphql query BatchGetTokenInfo { TokenInfo1: GetToken( + network: ENJIN + chain: MATRIX collectionId: 7153 # Specifies the collection ID for the first token - tokenId: { integer: 10 } # Specifies the unique identifier of the first token + tokenId: 10 # Specifies the unique identifier of the first token ) { supply # Returns the current supply of the first token } TokenInfo2: GetToken( + network: ENJIN + chain: MATRIX collectionId: 4553 # Specifies the collection ID for the second token - tokenId: { integer: 55 } # Specifies the unique identifier of the second token + tokenId: 55 # Specifies the unique identifier of the second token ) { supply # Returns the current supply of the second token } @@ -516,10 +508,10 @@ query BatchGetTokenInfo { { "data": { "TokenInfo1": { - "supply": 100 // Current supply of the first token + "supply": "100" }, "TokenInfo2": { - "supply": 200 // Current supply of the second token + "supply": "200" } } } @@ -533,33 +525,35 @@ Each token's data is aliased using `TokenInfo1` and `TokenInfo2`. #### Batch Mutation -A batch `mutation` allows you to send multiple mutation operations in a single request to reduce network overhead. - -Each mutation can have its own set of parameters and return data. +To submit multiple on-chain actions in a single transaction, use `CreateBatchTransaction` and pass an array of `TransactionInput` values via its `transactions` argument. The actions are bundled into one extrinsic and either all succeed or all fail together. ```graphql -mutation { - MintToken1: MintToken( - recipient: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" # Specifies the recipient's address for the first MintToken operation - collectionId: "7154" # Specifies the collection ID for the first MintToken operation - params: { - amount: 1 # Specifies the amount to mint for the first MintToken operation - tokenId: { integer: 6533 } # Specifies the unique identifier of the first token to mint - } - ) { - id # Returns the request ID for the first MintToken operation - } - MintToken2: MintToken( - recipient: "0x1234567890abcdef" # Specifies the recipient's address for the second MintToken operation - collectionId: "7155" # Specifies the collection ID for the second MintToken operation - params: { - amount: 2 # Specifies the amount to mint for the second MintToken operation - tokenId: { integer: 6534 } # Specifies the unique identifier of the second token to mint - } +mutation BatchMint { + CreateBatchTransaction( + network: ENJIN + chain: MATRIX + transactions: [ + { + mintToken: { + recipient: "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y" + collectionId: 7154 + params: { tokenId: 6533, amount: 1 } + } + } + { + mintToken: { + recipient: "efAnotherRecipientAddressHereXxxxxxxxxxxxxxxxxxxxxx" + collectionId: 7155 + params: { tokenId: 6534, amount: 2 } + } + } + ] ) { - id # Returns the request ID for the second MintToken operation + uuid + action + state } } ``` @@ -568,11 +562,10 @@ mutation { ```json { "data": { - "MintToken1": { - "id": "1234" // request ID for the first MintToken operation - }, - "MintToken2": { - "id": "1235" // request ID for the second MintToken operation + "CreateBatchTransaction": { + "uuid": "f7d3d9b8-1234-4abc-9def-0123456789ab", + "action": "Utility.batch_all", + "state": "PENDING" } } } @@ -580,37 +573,39 @@ mutation { -In this batch mutation, we have two separate `MintToken` mutations, labeled using the `MintToken1` and `MintToken2` aliases. - -Both mutations are minting two different tokens into two separate addresses. - -### Pagination - -Some GraphQL queries returns results in multiple pages. In GraphQL, these are called "Connections", and each of the elements in the connection is called "Edge". - -For example, the [GetWallet query](https://studio.apollographql.com/public/EnjinPlatform/variant/core/schema/reference/objects/Query?query=GetWallet) returns an object of type [Wallet](https://studio.apollographql.com/public/EnjinPlatform/variant/core/schema/reference/objects/Wallet). Wallet objects contains tokenAccount which is of type [TokenAccountConnection](https://studio.apollographql.com/public/EnjinPlatform/variant/core/schema/reference/objects/TokenAccountConnection). Since this object is of type 'connection', it uses pagination. +In this batch mutation, two `mintToken` actions are bundled into a single on-chain extrinsic. The Enjin Platform returns one `Transaction` for the batch as a whole. -By default, if you don't specify which page to query, the first page will be returned. -To check if the response contains another page for a certain object, use the pageInfo.hasNextPage property from the connection. +Alternatively, you can also send several independent top-level operations in one request using aliases, just like a batch query. Each operation then runs as its own transaction: ```graphql -query FetchingWalletTokens{ - GetWallet(account: "cxKnfok66R8BAuzGcypxqirYcqW7E9Spn4z5UZSVRuUHAcrTQ"){ - tokenAccounts{ - pageInfo{ - hasNextPage #Returns whether there's a next page or not +mutation { + Mint1: CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + mintToken: { + recipient: "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y" + collectionId: 7154 + params: { tokenId: 6533, amount: 1 } } - edges{ - node{ - token{ - tokenId - } - balance - } + } + ) { + uuid + } + Mint2: CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + mintToken: { + recipient: "efAnotherRecipientAddressHereXxxxxxxxxxxxxxxxxxxxxx" + collectionId: 7155 + params: { tokenId: 6534, amount: 2 } } } + ) { + uuid } } ``` @@ -619,60 +614,35 @@ query FetchingWalletTokens{ ```json { "data": { - "GetWallet": { - "tokenAccounts": { - "pageInfo": { - "hasNextPage": true // Indicates that there is another page available. - }, - "edges": [ - { - "node": { - "token": { - "tokenId": "5" - }, - "balance": "1" - } - }, - {...}, - {...} - ] - } - } + "Mint1": { "uuid": "a90ded41-4262-40a2-95c0-98255b660bf1" }, + "Mint2": { "uuid": "b8e3fa12-9876-4321-cdef-0fedcba98765" } } } ``` -By default, this connection returns 15 edges (tokens) per page. +### Pagination -:::tip -The amount of edges per page can be adjusted by using slicing. -::: +Some queries return more results than fit in a single response. The Enjin Platform provides two pagination styles depending on the query: + +#### Offset (page-based) pagination -To navigate to the next page, use pagination combined with the edge cursor. -First, add the cursor field to the query response. (Alternatively, you can add the pageInfo.endCursor field to fetch the cursor of the last edge in the connection) +Most plural queries — `GetTokens`, `GetCollections`, `GetListings`, `GetNominationPools` — accept `limit` and `page` arguments and return a flat array. Pass `page: 1` for the first page, `page: 2` for the second, and so on. When the response is empty (or contains fewer items than `limit`), you've reached the end. ```graphql -query FetchingWalletTokens{ - GetWallet(account: "cxKnfok66R8BAuzGcypxqirYcqW7E9Spn4z5UZSVRuUHAcrTQ"){ - tokenAccounts{ - pageInfo{ - hasNextPage - endCursor #Returns The cursor of the last tokenAccount in the current page - } - edges{ - cursor #Returns The tokenAccount cursor - node{ - token{ - tokenId - } - balance - } - } - } +query FirstPage { + GetTokens( + network: ENJIN + chain: MATRIX + collectionId: 7153 + limit: 15 # Up to 15 tokens per page + page: 1 # Start at page 1 + ) { + tokenId + supply } } ``` @@ -681,65 +651,39 @@ query FetchingWalletTokens{ ```json { "data": { - "GetWallet": { - "tokenAccounts": { - "pageInfo": { - "hasNextPage": true, - "endCursor": "eyJpZCI6Mjk0NzQsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0" // The cursor of the last tokenAccount in the current page - }, - "edges": [ - { - "cursor": "eyJpZCI6MzU3NCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", // The tokenAccount cursor - "node": { - "token": { - "tokenId": "5" - }, - "balance": "1" - } - }, - {...}, - {...}, - { - "cursor": "eyJpZCI6Mjk0NzQsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", // The tokenAccount cursor - "node": { - "token": { - "tokenId": "11" - }, - "balance": "1" - } - } - ] - } - } + "GetTokens": [ + { "tokenId": "0", "supply": "1000" }, + { "tokenId": "1", "supply": "500" } + ] } } ``` -Now we have the cursor of the last edge of the current page. -To navigate to the next page, add the `after` parameter to the `tokenAccounts` connection, and insert the cursor of the last edge of the current page. +To fetch the next page, send the same query again with `page: 2`, then `page: 3`, and so on. + +#### Cursor-based pagination + +A few queries — `GetTransactions`, `GetManagedWallets` — use a cursor instead of a page number, and return a wrapper object instead of a plain array. The wrapper exposes `data` (the items on this page), `perPage`, and `nextCursor` / `previousCursor` strings to navigate. ```graphql -query FetchingWalletTokens{ - GetWallet(account: "cxKnfok66R8BAuzGcypxqirYcqW7E9Spn4z5UZSVRuUHAcrTQ"){ - tokenAccounts(after: "eyJpZCI6Mjk0NzQsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0"){ #Indicates that we want to fetch tokenAccounts after the specified cursor. - pageInfo{ - hasNextPage - endCursor - } - edges{ - cursor - node{ - token{ - tokenId - } - balance - } - } +query FirstPage { + GetTransactions( + network: ENJIN + chain: MATRIX + limit: 15 + ) { + data { + uuid + action + state } + perPage + previousCursor + nextCursor # Use this on the next request to fetch the following page } } ``` @@ -748,44 +692,45 @@ query FetchingWalletTokens{ ```json { "data": { - "GetWallet": { - "tokenAccounts": { - "pageInfo": { - "hasNextPage": true, - "endCursor": "eyJpZCI6NTE2MzgsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0" - }, - "edges": [ - { - "cursor": "eyJpZCI6Mzc2MTYsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "token": { - "tokenId": "9" - }, - "balance": "1" - } - }, - {...}, - {...}, - { - "cursor": "eyJpZCI6NTE2MzgsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "token": { - "tokenId": "1" - }, - "balance": "1" - } - } - ] - } + "GetTransactions": { + "data": [ + { "uuid": "a90ded41-...", "action": "MultiTokens.mint", "state": "FINALIZED" } + ], + "perPage": 15, + "previousCursor": null, + "nextCursor": "eyJpZCI6OTk5LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9" + } + } +} +``` + + + +To fetch the next page, pass the previous response's `nextCursor` as the `cursor` argument: + + + +```graphql +query NextPage { + GetTransactions( + network: ENJIN + chain: MATRIX + limit: 15 + cursor: "eyJpZCI6OTk5LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9" + ) { + data { + uuid + action + state } + nextCursor } } ``` -To get all edges in the connection, continue navigating to the next page with a loop. -You'll know that you reached the last page when the `hasNextPage` returns `false`. +When `nextCursor` is `null`, you've reached the last page. *** From 57c4b8c205ff30561a00f5d21f013a20491276bb Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Tue, 12 May 2026 20:10:46 +0300 Subject: [PATCH 05/67] Rewrite v3 Using-the-Enjin-Platform page, drop v2/v3 framing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main change: rewrite docs/01-getting-started/04-using-the-enjin-platform.md for the v3 platform model. - Section 1: collapsed to a single platform URL with a network selector; dropped the v2 "three platform versions" callout (no per-network URLs, no enterprise on-prem). - Section 3: removed the manual-signing path (Enjin Wallet / Polkadot.js) since the platform no longer supports it. Daemon is the only signer, with Managed Wallet as a per-transaction override. - Section 4 Option A: refreshed UI screenshots; added the new Signing Account field with a link to the managed-wallets guide; replaced the inline "collection created" banner with the new Transaction Submitted modal. - Section 4 Option B: single endpoint, Bearer auth, CreateTransaction with flat createCollection input. Dropped c#/c++ SDK tabs (v3 SDK shapes not yet published). Added a callout linking to Using-the-Enjin-API and How-to-Use-GraphQL for context. - Section 5 ("Approve Platform Requests"): folded into a small info callout in section 4 — there is no manual approval step in v3, so the header was misleading. - Section 5 (was 6, Receive Transaction Information): GetPendingEvents doesn't exist in v3, so the API example is rewritten around GetTransaction(uuid). UI subsection points users at the Extrinsic Hash link on the Transactions page, which opens the transaction on Subscan for full event-level data (the platform UI doesn't currently expose events itself; chain-introspection queries are a known bug). Side cleanups across the three previously-committed v3 pages: removed "In v3" / "Wallet Daemon v3" / "Unlike previous versions of the platform" framing. These pages will become the main docs once the platform exits beta, so the v2-comparison language reads as outdated baggage. Also: gitignore widened from `v3-API.md` to `v3-*.md` to also cover the new `v3-MIGRATION.md` tracker file. --- .gitignore | 4 +- .../04-using-the-enjin-platform.md | 540 ++++++------------ .../01-how-to-use-graphql.md | 2 +- .../05-using-enjin-api/05-using-enjin-api.md | 2 +- .../06-using-wallet-daemon.md | 4 +- .../v3-create-collection-form.png | Bin 0 -> 65032 bytes ...v3-transaction-options-signing-account.png | Bin 0 -> 9680 bytes .../v3-transaction-submitted-modal.png | Bin 0 -> 19415 bytes .../getting-started/v3-transactions-page.png | Bin 0 -> 30491 bytes 9 files changed, 186 insertions(+), 366 deletions(-) create mode 100644 static/img/getting-started/v3-create-collection-form.png create mode 100644 static/img/getting-started/v3-transaction-options-signing-account.png create mode 100644 static/img/getting-started/v3-transaction-submitted-modal.png create mode 100644 static/img/getting-started/v3-transactions-page.png diff --git a/.gitignore b/.gitignore index 61bbbca..4d46dea 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -# v3 documentation migration notes (working file, not for upstream) -v3-API.md +# v3 documentation migration working files (not for upstream) +v3-*.md build-test/ diff --git a/docs/01-getting-started/04-using-the-enjin-platform.md b/docs/01-getting-started/04-using-the-enjin-platform.md index a4fae39..78d0039 100644 --- a/docs/01-getting-started/04-using-the-enjin-platform.md +++ b/docs/01-getting-started/04-using-the-enjin-platform.md @@ -15,59 +15,35 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa ## 1. Set Up an Account -To get started, go ahead and create an account on the and **verify your email address.** In this tutorial, we'll be using the [Enjin Platform Cloud (Testnet)](https://platform.canary.enjin.io). +To get started, go ahead and create an account on the [Enjin Platform Cloud](https://platform.beta.enjin.io/) and **verify your email address.** -:::info There are three versions of the Enjin Platform to choose from: -- The [Enjin Platform Cloud (Testnet)](https://platform.canary.enjin.io), connected to the Enjin . -- The [Enjin Platform Cloud (Mainnet)](http://platform.enjin.io/), connected to the Enjin . -- The [Enterprise On-Prem Enjin Platform](/05-enjin-platform/04-enterprise-on-prem/04-enterprise-on-prem.md) which you can run locally to connect to either network. -::: +The Enjin Platform is chain-agnostic — a single dashboard covers both networks. Use the **network selector in the top-right corner** of the platform UI to switch between **Canary** (testnet) and **Enjin** (mainnet). We recommend starting on Canary while you build and test. ### Things you should know - is , which is why we recommend building your initial proof-of-concept on Enjin – the fast and free environment for testing Enjin's tools. -- Every "" request (i.e, minting a ) must be signed by a . +- Every "" request (i.e, minting a ) must be signed by a . On the Enjin Platform, signing is handled automatically by the [Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) (or, optionally, by a [Managed Wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) that you select per-transaction). ## 2. Set Up an API Token -Once your account is created, create a new API token by clicking the "**Create API Token**", which can be found on the [Account Settings](https://platform.canary.enjin.io/settings) page. - -![Creating an API token on the Enjin Platform](/img/getting-started/create-api-token.gif) - -Copy and save that token somewhere safe, you'll need that to automatically approve transactions using a . +Once your account is created, create a new API token by clicking the "**Create API Token**" button on the [Account Settings](https://platform.beta.enjin.io/settings) page. +Copy and save that token somewhere safe — you'll need it to configure your and to authenticate API requests. :::tip Note You can always revoke the token and create another one in the future. ::: -## 3. Set Up a Wallet +## 3. Set Up the Wallet Daemon -Transactions can be approved in 2 different ways: - -- **Manually:** Using the [Enjin Wallet](https://enjin.io/products/wallet) / [Polkadot.js](https://polkadot.js.org/). -- **Automatically:** Using the [Enjin Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md). - -:::tip Start by manually approving transactions -If you're using the Enjin Platform for the first time, we recommend [Using Enjin Wallet / Polkadot.js](#a-using-enjin-wallet--polkadotjs) -Enjin wallet app is available on both [iOS](https://enj.in/ios-wallet) and [Android](https://enj.in/android-wallet). -::: +Every on-chain operation initiated through the Enjin Platform must be signed before it can be broadcast to the blockchain. The Enjin Wallet Daemon does this for you automatically. -### A. Using Enjin Wallet / Polkadot.js +To download and configure the Wallet Daemon, head over to the [Wallet Daemon Setup](/01-getting-started/06-using-wallet-daemon.md) page. The daemon needs to be configured with the API token you created in step 2 — that's how it knows which platform account to sign for. -Connect your wallet by clicking the "**Connect Wallet**" button in the top right corner and follow the on-screen instructions. -The also offers gamers a user-friendly, secure, and rewarding experience, ensuring their continuous engagement with your game. +Once the daemon is running and connected, your Platform account is ready to use. -![Connecting a wallet app on the Enjin Platform](/img/getting-started/connect-wallet-app.gif) - -### B. Using Daemon Wallet - -The Enjin Wallet Daemon can automatically sign requests for you. - -To download and set up wallet daemon, head over to the [Wallet Daemon Setup](/01-getting-started/06-using-wallet-daemon.md) page. -To ensure that only authorized requests are approved, the Wallet daemon needs to be configured with an Enjin Platform API Token. -Create an API Token on the [Account Settings](https://platform.canary.enjin.io/settings) page and set the Platform API Token in your Wallet Daemon configuration. - -Once your wallet daemon is connected, your Platform account is set up and ready to use! +:::info Signing with a Managed Wallet +On most transaction creation forms in the Platform UI, you can override the default signer using the **Transaction Options → Signing Account** field. Setting it to a Managed Wallet's address signs the transaction with that wallet's stored key instead of with the Wallet Daemon's wallet. See [Using Managed Wallets](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) for details. +::: ## 4. Create Platform Requests @@ -87,173 +63,100 @@ However, if you need to make a Platform request programmatically, you can do tha ### Option A. Using the Enjin Dashboard -In the Platform menu, navigate to "**[Collections](https://platform.canary.enjin.io/collections)**". Then, click the "**[Create Collection](https://platform.canary.enjin.io/create/collection)**" button. +In the Platform menu, navigate to "**[Collections](https://platform.beta.enjin.io/collections)**" and click the "**Create Collection**" button. -![Create collection form on Enjin Platform](/img/getting-started/create-collection-form.png) +![Create collection form on Enjin Platform](/img/getting-started/v3-create-collection-form.png) -From here, you can customize your collection's Mint Policy, Market Policy, Explicit Royalty Currencies (optional), and Attributes. +From here, you can customize your collection's Mint Policy, Royalties, Explicit Royalty Currencies (optional), and Attributes. -Once you're satisfied with the options, click on the "**Create**" button at the bottom right corner to create the request. +#### Selecting the signing account -![Collection created banner on Enjin Platform](/img/getting-started/collection-created-banner.png) +Toward the bottom of the form, the **Transaction Options** section lets you choose which wallet should sign this transaction. By default this is set to the Wallet Daemon's address, but you can paste a [Managed Wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) address here to sign with that wallet instead. + +![Transaction Options Signing Account field](/img/getting-started/v3-transaction-options-signing-account.png) + +Once you're satisfied with the options, click the "**Create**" button at the bottom right to submit the request. You'll see a confirmation modal with the new transaction's UUID: + +![Transaction Submitted modal showing the new transaction UUID](/img/getting-started/v3-transaction-submitted-modal.png) + +Click **View Transaction** to jump straight to the Transactions page and watch it move from `PENDING` → `BROADCAST` → `FINALIZED` as the daemon signs and the chain finalizes it. ### Option B. Using the Enjin API & SDKs To create a Platform request programmatically, use the API, or one of the s. -This mutation will set up a new transaction that once finalized on-chain will contain the new collection id in the transaction . +This mutation will set up a new transaction that, once finalized on-chain, will produce a new collection. The collection's ID can be read from the transaction's events afterward. -:::tip Test your requests in the GraphiQL Playground: -- [Testnet](https://platform.canary.enjin.io/graphiql) -- [Mainnet](https://platform.enjin.io/graphiql) +:::info First time using the API? +For a step-by-step walkthrough of the endpoint, authentication, and the `network` / `chain` arguments used below, see [Using the Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md). If you're new to GraphQL itself, the [How to Use GraphQL](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md) guide is a good starting point. ::: -:::tip -If you are not using the playground, you need to add your API token to the headers to be authenticated, like so: -```json -"Authorization": "" +:::tip Test your requests in the GraphiQL Playground +[Open the Enjin Platform GraphiQL Playground](https://platform.beta.enjin.io/graphiql) +::: + +:::tip Authentication +If you are not using the playground, you need to add your API token to the headers, prefixed with `Bearer`: +``` +Authorization: Bearer ``` ::: +Every on-chain action runs through a single `CreateTransaction` mutation. The specific action is selected by which field is set on the `transaction` argument — here, `createCollection`. + ```graphql mutation CreateCollection { - CreateCollection(mintPolicy: { forceCollapsingSupply: false }) #Set to true to enforce collapsing supply mint policy - { - id - method + CreateTransaction( + network: CANARY # or ENJIN for mainnet + chain: MATRIX + transaction: { + createCollection: { + forceCollapsingSupply: false # Set to true to enforce collapsing supply mint policy + } + } + ) { + uuid + action state } } ``` -```json -curl --location 'https://platform.canary.enjin.io/graphql' \ +```bash +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation Infuse(\r\n $collection_id: BigInt!\r\n $token_id: EncodableTokenIdInput!\r\n $amount: BigInt!\r\n) {\r\n Infuse(collectionId: $collection_id, tokenId: $token_id, amount: $amount) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"forceCollapsingSupply":false}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the mutation -var createCollection = new CreateCollection() - .SetMintPolicy(new MintPolicy().SetForceCollapsingSupply(false)); //Set to true to enforce collapsing supply mint policy - -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -createCollection.Fragment(transactionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendCreateCollection(createCollection); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cplusplus -#include "EnjinPlatformSdk/CoreMutations.hpp" -#include -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Setup mutation data - shared_ptr mintPolicy = make_shared(); - mintPolicy->SetForceSingleMint(make_shared(false)); - - // Setup mutation - CreateCollection createCollection = CreateCollection(); - createCollection.SetMintPolicy(mintPolicy); - - // Define and assign the return data fragment to the mutation - shared_ptr transactionFragment = make_shared(); - transactionFragment - ->WithId() - .WithMethod() - .WithState(); - - createCollection.SetFragment(transactionFragment); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>>> futureResponse = SendCreateCollection(*client, createCollection); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& transaction = gqlResult->GetData()->GetResult(); - - std::cout << to_string(transaction->GetId().value()) << std::endl; - std::cout << ToString(transaction->GetMethod().value()) << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} +-H 'Authorization: Bearer ' \ +-d '{"query":"mutation { CreateTransaction(network: CANARY, chain: MATRIX, transaction: { createCollection: { forceCollapsingSupply: false } }) { uuid action state } }"}' ``` ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + }, body: JSON.stringify({ query: ` mutation CreateCollection($forceCollapsingSupply: Boolean) { - CreateCollection( - mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } + CreateTransaction( + network: CANARY + chain: MATRIX + transaction: { + createCollection: { forceCollapsingSupply: $forceCollapsingSupply } + } ) { - id - method + uuid + action state } } `, variables: { - forceCollapsingSupply: false //Set to true to enforce collapsing supply mint policy + forceCollapsingSupply: false } }), }) @@ -265,23 +168,30 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation CreateCollection($forceCollapsingSupply: Boolean) { - CreateCollection( - mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } + CreateTransaction( + network: CANARY + chain: MATRIX + transaction: { + createCollection: { forceCollapsingSupply: $forceCollapsingSupply } + } ) { - id - method + uuid + action state } } `, variables: { - forceCollapsingSupply: false //Set to true to enforce collapsing supply mint policy + forceCollapsingSupply: false } }, { - headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + } }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -293,48 +203,42 @@ import requests query = ''' mutation CreateCollection($forceCollapsingSupply: Boolean) { - CreateCollection( - mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } + CreateTransaction( + network: CANARY + chain: MATRIX + transaction: { + createCollection: { forceCollapsingSupply: $forceCollapsingSupply } + } ) { - id - method + uuid + action state } } ''' -variables = {'forceCollapsingSupply': False} #Set to true to enforce collapsing supply mint policy +variables = {'forceCollapsingSupply': False} -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +response = requests.post( + 'https://platform.beta.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={ + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + } ) print(response.json()) ``` -A WebSocket event will also be fired so you can pick up the collection in real time by listening to the app channel on the WebSocket. - -## 5. Approve Platform Requests - -Once a Platform request is created, all that remains is to approve it, which will sign the transaction and broadcast it to the Enjin Blockchain - -Transaction requests are displayed in the "**[Transactions](https://platform.canary.enjin.io/transactions)**" menu. +The response includes a `uuid` you can use to query the transaction's status as it moves through the daemon-signing → broadcast → finalization lifecycle. -![Pending create collection txn on Enjin Platform](/img/getting-started/pending-create-collection-txn.png) - - -Since this transaction is a , you will need to sign the transaction using your Wallet. - -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. - -![Approving the create collection txn on Enjin Platform](/img/getting-started/approving-create-collection-txn.gif) - -And that's it! The transaction is then broadcasted to the Enjin Blockchain, and once approved, it will be included in a . +:::info Signing happens automatically +Once a request is created — whether through the dashboard or the API — the Wallet Daemon (or the [Managed Wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) you specified in the `Signing Account` field) picks it up, signs it, and broadcasts it to the chain. There is no manual approval step. The transaction will move from `PENDING` → `BROADCAST` → `FINALIZED` as it's processed. +::: -## 6. Receive Transaction Information +## 5. Receive Transaction Information There are 3 ways to receive the transaction status and information: @@ -344,113 +248,73 @@ There are 3 ways to receive the transaction status and information: ### Receive Transaction Information Using the Platform User Interface -To check the transaction status, head over to the [Transactions](https://platform.canary.enjin.io/transactions) page locate the transaction and click on the button to it's right to show more details. -Once the transactions approves on-chain, the transaction will get updated with the `FINALIZED` state and the transaction events will be displayed. -In the events, we can find useful information, such as the newly created collection ID. +To check the transaction status, head over to the [Transactions](https://platform.beta.enjin.io/transactions) page. Each row shows the transaction's `UUID`, `Action` (e.g. `MultiTokens.create_collection`), `State`, and `Extrinsic Hash` once it has been broadcast. -![How to find your newly created collection ID](/img/getting-started/find-newly-created-collection-id.gif) +![Transactions page showing a finalized create_collection transaction](/img/getting-started/v3-transactions-page.png) -### Receive Transaction Information Using the Enjin API & SDKs +Once the state reaches `FINALIZED`, click the row's **Extrinsic Hash** link to open the transaction on Subscan (the blockchain explorer). The **Events** tab there lists everything the transaction emitted on-chain — for a collection-creation transaction, that's where you'll find the newly minted **collection ID**. -To receive the transaction information programmatically, You can use the `GetTransaction` query and set the `id` to the ID received in the `CreateCollection` mutation. -But for this example, we're going to use the `GetPendingEvents` query, which returns all of the events emitted to your platform account. +### Receive Transaction Information Using the Enjin API & SDKs -:::info This query might return items in multiple pages using Connections -To learn how to use GraphQL cursors for pagination, head to [Using the API --> Pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination). -::: +To receive transaction information programmatically, use the `GetTransaction` query. Pass the `uuid` returned by `CreateTransaction` and request whichever fields you need — `state`, `extrinsicHash`, `action`, and so on. **Query:** ```graphql -query GetPendingEvents{ - GetPendingEvents{ - edges{ - node{ - name - data - } - } +query GetTransactionStatus($uuid: String!) { + GetTransaction( + network: CANARY + chain: MATRIX + uuid: $uuid + ) { + uuid + action + state + extrinsicHash + createdAt + updatedAt } } ``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query": "query GetPendingEvents { GetPendingEvents { edges { node { name data } } } }"}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; -var filterInputArray = new StringFilterInput[] +**Variables:** + +```json { - new StringFilterInput() - .SetType(FilterType.And) - .SetFilter("collection;2000"), -}; - -// Setup the query -var getPendingEvents = new GetPendingEvents() - .SetChannelFilters(filterInputArray); - -// Define and assign the return data fragment to the query -var pendingEventsFragment = new ConnectionFragment() - .WithEdges(new EdgeFragment() - .WithNode(new PendingEventFragment() - .WithId() - .WithUuid() - .WithName() - .WithSent() - .WithChannels() - .WithData() - ) - ); - -getPendingEvents.Fragment(pendingEventsFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendGetPendingEvents(getPendingEvents); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); + "uuid": "12f22f91-82df-4b60-a90c-bf72b508a17c" +} ``` - -```cplusplus - + +```bash +curl --location 'https://platform.beta.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer ' \ +-d '{"query":"query { GetTransaction(network: CANARY, chain: MATRIX, uuid: \"12f22f91-82df-4b60-a90c-bf72b508a17c\") { uuid action state extrinsicHash } }"}' ``` ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + }, body: JSON.stringify({ query: ` - query GetPendingEvents{ - GetPendingEvents{ - edges{ - node{ - name - data - } - } + query GetTransactionStatus($uuid: String!) { + GetTransaction(network: CANARY, chain: MATRIX, uuid: $uuid) { + uuid + action + state + extrinsicHash } } `, + variables: { uuid: '12f22f91-82df-4b60-a90c-bf72b508a17c' } }), }) .then(response => response.json()) @@ -461,21 +325,23 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` - query GetPendingEvents{ - GetPendingEvents{ - edges{ - node{ - name - data - } - } + query GetTransactionStatus($uuid: String!) { + GetTransaction(network: CANARY, chain: MATRIX, uuid: $uuid) { + uuid + action + state + extrinsicHash } } `, + variables: { uuid: '12f22f91-82df-4b60-a90c-bf72b508a17c' } }, { - headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + } }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -486,21 +352,25 @@ axios.post('https://platform.canary.enjin.io/graphql', { import requests query = ''' - query GetPendingEvents{ - GetPendingEvents{ - edges{ - node{ - name - data - } - } - } +query GetTransactionStatus($uuid: String!) { + GetTransaction(network: CANARY, chain: MATRIX, uuid: $uuid) { + uuid + action + state + extrinsicHash } +} ''' -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} +variables = {'uuid': '12f22f91-82df-4b60-a90c-bf72b508a17c'} + +response = requests.post( + 'https://platform.beta.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={ + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + } ) print(response.json()) ``` @@ -512,77 +382,27 @@ print(response.json()) ```json { "data": { - "GetPendingEvents": { - "edges": [ - { - "node": { - "name": "platform:transaction-created", - "data": { - "id": 1648, - "state": "PENDING", - "method": "CreateCollection", - "idempotencyKey": "00e32324-acfa-4ddb-8b7d-6dd8d0b4f694" - } - } - }, - { - "node": { - "name": "platform:transaction-updated", - "data": { - "id": 1648, - "state": "BROADCAST", - "method": "CreateCollection", - "result": null, - "transactionId": null, - "idempotencyKey": "00e32324-acfa-4ddb-8b7d-6dd8d0b4f694", - "transactionHash": "0xeb2510094c53317e19ca188b5ba8a21b81f36f4bc1be09e079b46ee7b9bce754" - } - } - }, - { - "node": { - "name": "platform:collection-created", - "data": { - "owner": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", - "collectionId": "38224", - "idempotencyKey": "00e32324-acfa-4ddb-8b7d-6dd8d0b4f694" - } - } - }, - { - "node": { - "name": "platform:transaction-updated", - "data": { - "id": 1648, - "state": "FINALIZED", - "method": "CreateCollection", - "result": "EXTRINSIC_SUCCESS", - "transactionId": "1404095-2", - "idempotencyKey": "00e32324-acfa-4ddb-8b7d-6dd8d0b4f694", - "transactionHash": "0xeb2510094c53317e19ca188b5ba8a21b81f36f4bc1be09e079b46ee7b9bce754" - } - ] + "GetTransaction": { + "uuid": "12f22f91-82df-4b60-a90c-bf72b508a17c", + "action": "MultiTokens.create_collection", + "state": "FINALIZED", + "extrinsicHash": "0xd43b83...014b1365", + "createdAt": "2026-05-07T16:36:33Z", + "updatedAt": "2026-05-07T16:36:51Z" } } } ``` -The first event with `PENDING` state was emitted when the `CreateCollection` platform request was created. -The second event with `BROADCAST` state was emitted when the transaction was signed and broadcasted to the Enjin Blockchain. -The third event was emitted when the collection was created on-chain, and has the `collectionId` in it's data. -The forth and final event with `FINALIZED` state was emitted when the platform request was finalized. +A `state` of `FINALIZED` means the transaction has been included in a finalized block. To list multiple transactions instead of fetching one by UUID, use the cursor-paginated `GetTransactions` query — see [Pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination) for the response shape. :::info Note -For the full list of platform states, check the [Enjin API --> Important Arguments](/03-api-reference/04-important-arguments.md#state) page. -::: - -:::tip Acknowledging Events -To remove the pending events from the queue after fetching them, set the `acknowledgeEvents` parameter to true +For the full list of platform states, check the [Enjin API → Important Arguments](/03-api-reference/04-important-arguments.md#state) page. ::: ### Receive Transaction Information by Listening to Platform Websocket Events -You can also subscribe for events via Websocket. for more information, check out the [Websocket Events](/05-enjin-platform/03-working-with-events.md) +You can also subscribe to events via WebSocket — useful when you want to react to transaction state changes without polling. For more information, check out the [Websocket Events](/05-enjin-platform/03-working-with-events.md) guide. :::tip What's Next? Learn how to [Use the API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md). diff --git a/docs/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md b/docs/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md index 49f6e17..c01adce 100644 --- a/docs/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md +++ b/docs/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md @@ -426,7 +426,7 @@ The `supply` field indicates the data we want to receive in the response. #### Mutation: -A `mutation` is used to modify state on the Enjin Platform — typically by creating an on-chain transaction. In v3, every blockchain operation is performed through the `CreateTransaction` mutation, with the specific action selected inside its `transaction` argument (`mintToken`, `createCollection`, `transferToken`, and so on). +A `mutation` is used to modify state on the Enjin Platform — typically by creating an on-chain transaction. Every blockchain operation is performed through the `CreateTransaction` mutation, with the specific action selected inside its `transaction` argument (`mintToken`, `createCollection`, `transferToken`, and so on). In this example, we're minting a new token. diff --git a/docs/01-getting-started/05-using-enjin-api/05-using-enjin-api.md b/docs/01-getting-started/05-using-enjin-api/05-using-enjin-api.md index 5f48e15..9ca124b 100644 --- a/docs/01-getting-started/05-using-enjin-api/05-using-enjin-api.md +++ b/docs/01-getting-started/05-using-enjin-api/05-using-enjin-api.md @@ -57,7 +57,7 @@ For developers looking for a more integrated experience, Enjin also provides off ### GraphQL Endpoint -Unlike previous versions of the platform, which exposed separate endpoints per feature area and per network, the Enjin Platform now serves all queries and mutations from a **single GraphQL endpoint**: +The Enjin Platform serves all queries and mutations from a **single GraphQL endpoint**: :::note GraphQL Endpoint `https://platform.beta.enjin.io/graphql` diff --git a/docs/01-getting-started/06-using-wallet-daemon.md b/docs/01-getting-started/06-using-wallet-daemon.md index 7a6328f..9e703d2 100644 --- a/docs/01-getting-started/06-using-wallet-daemon.md +++ b/docs/01-getting-started/06-using-wallet-daemon.md @@ -41,7 +41,7 @@ The code repository can be found at https://github.com/enjin/wallet-daemon/ It is recommended that the Enjin Wallet Daemon is installed and ran in isolation. This means running it on a dedicated server. The daemon itself is incredibly light-weight and does not require any extensive resources. :::info Network-agnostic -The Wallet Daemon v3 is network-agnostic — it does not need to be configured for a specific chain. A single daemon instance signs transactions for all Enjin networks (Enjin Matrixchain, Enjin Relaychain, Canary Matrixchain, and Canary Relaychain). Each chain just maps to a different SS58 address derived from the same wallet, all of which are printed when the daemon starts. +The Wallet Daemon is network-agnostic — it does not need to be configured for a specific chain. A single daemon instance signs transactions for all Enjin networks (Enjin Matrixchain, Enjin Relaychain, Canary Matrixchain, and Canary Relaychain). Each chain just maps to a different SS58 address derived from the same wallet, all of which are printed when the daemon starts. ::: The recommended way to run the daemon is to [download a prebuilt binary](#download-the-binary) for your operating system. If you'd rather run it inside Docker, see [Running with Docker](#running-with-docker) at the bottom of this page. @@ -138,7 +138,7 @@ The seed file is encrypted with the `KEY_PASS` env var. Make sure `KEY_PASS` is ### Running with Docker -If you'd prefer to run the daemon inside Docker rather than as a standalone binary, clone the `3.x` branch of the repository (the v3 development branch) and use the included [`docker-compose.yml`](https://github.com/enjin/wallet-daemon/blob/3.x/docker-compose.yml). +If you'd prefer to run the daemon inside Docker rather than as a standalone binary, clone the `3.x` branch of the repository (the active development branch) and use the included [`docker-compose.yml`](https://github.com/enjin/wallet-daemon/blob/3.x/docker-compose.yml). ```bash git clone --branch 3.x https://github.com/enjin/wallet-daemon.git diff --git a/static/img/getting-started/v3-create-collection-form.png b/static/img/getting-started/v3-create-collection-form.png new file mode 100644 index 0000000000000000000000000000000000000000..121f0180c3f6ed2cabc7cbbbfa18145d00e9afc6 GIT binary patch literal 65032 zcmcG0XH*kwxGsw5Rw>(pY^fGdI-+z4h=>SC?=>nS5UM~xLWvbr6qKrzgkBOLv;ZN5 zs0b(})DS75NC_cy2%+Z=``mTbS+}hF%9AeflZ-2Dr6{4q~%}KG*n1>Wztdx_H?!EWWNnJcaz2zhB+RRfIqE1;; z1BNBc{rGci>s|=nZ)Oih7;XJh%nEYFuow$==Jr#61MT79_$cnsanLdj4(DHg0i6MU z|NNbF=-_(of+Xj`_5MxsgZ^=ReU`<2aD8@2_x!OgyjrVxM%G%lufrD=q>B~EsUpdRkk01GdgSZ6Tsq2@Y62;$nQQ0){5s5^~ zp3`yIm0ew1>*Er>955K9mO)eviE#o-3y5VyxK3gHdjjxOU;@Ibv1JbP#Xb4A1t~ zR!<^#fB7>G_I1769mH6H*l^fAuwpr!4|rtEZZOg}`98o!P`EM9KhE(vYmFD-K1G_@NKv>RMh|i@-2e zPx9zWO6?1LhhME7tdBPf=;ziizglCDt-TV%-BDmx#{^@9}UhT!n`p?Mn-3yF=eS*7RC6?|r?(Xfi@56)a$tpsxf~qU#6JnA`T0|S`YFIG#he9!Wf6kc({GlL zmX}hr<0R*dWI6Z1|LP#LamZ^=E`jxlG5pD#9M4cvAzY98{eWkin`vSxRq?{WUa-{H z#YxwlIwsE(#R9g4L$icL=w`5;tn*5M9T)7xBSEU7P@+Zi^!Cz_Ueh~q0Rs4klM zz4n@xD0!hocd&E!rHAX$fG3~ftd4eI)OLaOI&*3Bug1_KQyboc4gPgHj;OOEe{jS7 zMW8#uDw%w!Zc}Vh!_EtR z8}oa*Ncc+fK@ozW6hd?L&kGiym7*$=3@Sheo?^LK$1>s)sh;I}aN=$esg^ zg9X&pWtPyk>%G5yOI?^ZIVnl9&ebJ|pMxXy!}pwrL8l__W`5-evdFKD7q)uZcV*@e zBsHnQ)=;c;MK=DQ{c=A>Algm{yt*X-rsqy!Pw@EW)c9i7GKSww@X64AKaq&6zVjg4 zq(8sCwC$IAL9Ri3ar@Hg{+b2oiIb&Uj7a%a(p3A{7_lR=%FbMcb%(r>m67=gBga-R zTW~V7-kONi(J6K-yfYVJ;P%=eu!(OhNM0A$FukS^$Xn;*f3-E1svLC>x|@<`=Zdgk z9Couw4%JRLdv549ra?m7eB)l4o(CV`I&#T zOOK@|^1*v_Qrx9)$c;HeZNGSJZNt_u(z4}fDE{aSB+FJtQgZ#sBR91&dX_9@DQaAO zpYJaY4)AxG=!kS8Jl`xO<;mRodF3K8ANevBU;$)F_-n*Cm~9cSGMiI}#SQsDYTf<4 zp|W9XzSGm0J-#k}rwN zED;TqJK7%WL$+85CsAr=Lh4FzdVzmHk>r|A=nMqeUsJlKHT0v}c36dC^V>K>Ei1mT z%r|{tX3lY`;bmpm=Z)s&zI`pZuq_Yw-G~-v=;-w*?OX{3b_sHW_67aSk#4!Wc!=X! z(ES}}v(Sb7PsX7}s(d|bn6ADYf#@P9w_lI_V%t-~17&cxCZ}^u-rrbXq}GmE)p#uN zrER6!DLwu9Gti@Pw2Cq~AW|9mO`U7nIP4_?nEKFj`ScDinz##@-a$W2sFM7EaNo=@ z-qJkI!O`Z{H@5;G(21)dZ$h1zORBAN*`uI-#R9idQDWTl_~$s4-C9ioqIiHvc0`Vgjf%AM{2kyjBXEhi-$G3@`+}{ zF-}D>Lyu?sq;4=-xg^x(v8dAemuyt$unK*wDqPZ&6$skgr9~&RW=rF5RWIXRG&Wa# zPt1+l5gzsF3`8z?RnVJQHh-(%b-4dB7D_wI(j;FhSXP)+o}}&y9v_! zL)8PWEw-553T(LXGY@)ZPg2b1PBn=QB(cMLolC=yu3Ol9o;m3D?vo3x*VdRa3SH*F z4v=UPI|Zo%ECqp`qae-8#P4tM8X=H8gH~`ZQP!{U-~|3{mEZNlp;Y;atv5(xKkfSS ztypI3KQDNY0Tpn-8oLR_nIy6`Y^h}@ZS1Gy1a)fS^FTw;uibe~ZfT%D=k&p$=?Hd^ zm?4;fW~-xfVmbz5Sv5An_C;j`mehwv)80j7(u~6(Dh6>r+nXvf#0;OEI94jX8St`E zI>mYymynpBzQ6vgj9Fr}Cw-#5<)iQI_7)_oZo^u`=OH#W;-z7B+%h-ysqDwF^=oC(|=xt*hzhSOec)p0z2rWg8{0 zZ#k9@e3%=P>Bn2}T#K2OQ4~+tn*hY14KN>2Kt5|5xiW5m+Fi7oiYcBmVAqvb_gO&{ za0JfeK$_;Nw>CsO;}29Rf~A13wMi$>Wo5t%ZV%TYt_d(k4KB5 z^xC&GcL_SGD5~5`rR@)$8qW7!iwk~NKl$!5r{nt}WKe#{)cZYQx$a${O7VrsNd7!b zof7gr%OPmKN3KwIO8Z-A2v`fb~CR!=S=0*i9&w~6u4MgoKEI0)7 z-nfJu&3Zy@rI+@B*4uPO$=m(pH0R{EuQ1^^<3q~nVhkP1T(Wom7#~@zsq+IQT2U5k zco7%;8A_k;_H&6!9Y5X_dV>0|3Xz%sT3RA)6HF%c7e zZ&_;aC>{16nS)V)1@7KJ9LH;QJ-Uh^16G2hZ1vlM-LXV%#gk7^OR_Pr!@!~sad-SI#)ZnG5X-9(JOYO+e#%LpamObo4WR%PZ5Q>C@aPk-L+qWxOwlBDMu(3G60qEN1DqbJg8w7X>~ z@R5v)&Hg4q3yyh*bK-m`KLg)K*}YxR+#>syJ=>P+v=K-~!a-d9{!;91Md^x2k1{~N zNM4?uDXfl~)Z!tMQA_Oogq9DYg`X56verMQoJj0ns1WFv+07ai95Jrtc0tFLiQ=T+ zHW(zvV3Sp}(PLM+`>S(n>G3ulA`e2xFs@wcm0Zl8%qKHLX_P#}L>cctWh#_n-*ko+ zsA<}3ct(S;y2_2o@;Y~JDDQi{@i$P?$X?r3T(tdx!Ge9bb*Za^(&83UtK1llt3i`> z)ny3td5&^!F9<1ckZs;eoeYB_Q0(f43se^}T1YM<5b3CJTC>LKbigGF}3HA*|YYV3q{bB&G*I#^>au)P3pI6krc!H8t zwrF9Qb=c72@@XiVj`Qf}W|dFn#Sd8jlhSl6{V7)`f4e|fr!?q~1#Jq9eI5Q2N~)MV zFvzQcrNRWv*5ogB?Jl(ko9x;A$to8-AX*}{3V%B1q0{;m1?Zy}Ptlmgp8wQJ|7PJT zqwS3aq{AO=Zu$|}tN&2L+L!v<_TC3E#^|IcOAvhc&l-lqY)qn&-LMWirfQ*%qFf&) zq|XziEv#TTr0SHJR%>Csc6?}7UbE3@iMag6Pxan9KmV`+i%>qgqduqC9*n0sFpdv- z4i~G#->~j+*F15iSN*OfR`i9hmZyN$XN5G~t)BB@GW{guDVYjBR>=lgnnqJJaF;Xe z*X9-_QP?KJPlSUbOGQfl<3daqbMx*(G*$A-}CRjSM~s6<0%1C zr^39_u2h_=DXsuGERm1YY5`xQ*-CXy0rM0=+<0x+FtU5%M68zCp>(k7q$}Vdx{Bec z@_1_5ot6vsoL6)j@f|nX&Uj$Uu8kn#fu%8!t{?<!S!9QkoXP$KKI>q{o<}@B=_=Cbdx3xWbp5>EhF?_^NMW$8K|D^dwnxjKZZCXvD z_dZL+4Lr-EB>TvxiZjW>F*MZ9bN9KipqiTW7;n4-elFi877CZKGjH^*@%|^&jOXsI z<1a(f8_oN4SBWykBOA{y6xYpz59K&vEr$+GYVu}i5I6Jp_rDH$*vsApXMgS1>}WP+ z3!xX%-+O4@w2S|OkrWx&pC%@DCbnRbnFaQPjmqFJ-tHoG7Nt%3>+xxf)Zkz2S}-b7 zl@(eRp|8%iFz>$~w4xt+iE<^j`4VsdC<-QCyo=~JGrcw)pk{4pnKKT9b^-{9v&xBB z>3WCN2SQiXJqCD}vVUQSSra0V4bgo|)S0J#F4KbsaZ(LloqWg40G0Si+EfesvT&oOIS&e^&#@7@YdkubhlMmb}!1GdY2DQCqt=kN%lqw%gAgPZU zdtfQLR^6wZ3-ef=URnarAaJ%UNig>`(;QrPSY%Mc?99kT2V0(}Ssu-2t{l-vvChOY z2Tf^HPV^mOfBM_9YhK}Ym@>JKu#SJFzh*@+b|6tzZMaLQl)|@Uvu#N`*&s2w)*D17 zJguG4vKCBmGl+L5G)ar&e)`;+*fdBG>;8K8?e=Wih942fVgMzlTSW)$1U zI|ccga5i7;o)k#H&}nq1E-g9>EW`NPG^aM>f5NshQ007$AX6189Aq!j>SN6yt!08k zh3?z8o=-?7+*U91xA@h?K5YdBq)dB)_w2?uxixvlN9S}2-r318KMJUi2{_n)zEm>+ zg>c|ZGdxL(wA*0TdMWmzLzzbcwafM@@2u4f*&>kVe5jed$}}5Ml6(<&9jhTp;n`3X zNM=yn^l5ur%l_^jgaP&W;aHElwGZ`P-9kgt(s801;3}B)KZ)dsS#MZSgdNsTSUj_a z{zlGjpdi0X)qg7_-fkzTW)kT*Pnv%a@4c9}QF-+4i4!)o6*JFgc)$ z>K@n&PmWq9!f>&nE{so?$@q%AEL*|lV|siMg->7`rs%WxZI&LeQW}p1fa~K@YU%?9 zbx=1g%@f`AYbxL(=?>K#+2#p#F zF|J`@(Pjhd*ZDBPFF;xdcEg{v>G zda`3tliZ1ooaRopNA{43%GUWhISUwN$_AyS-W6$%XwukNi?{6O(BBJr_JWz~0}eyX z9C&Gv9|xW_fIOsrkr=WYRd7BPirIQM9jn#lzEcTwEnPo3^4u!L?X_#Wor`By>CLbm zZJDkB8o@~Cb557`=e!*ZM(L7qp{n<-E=9)%=Cd0eM_)^q*t|FzEVW?Kcwb<>$mC-Q zv(p{>Jp3PKY9%B(%CRvoc(&SOmKkC5l)s9d-=`8<6m>f+AqnAZSPrzw?(kaByJeAx zCG$)O)ZFj9l}h#=B<(}}G?e*2&GIO$>8j0WqxTO1F$O2)^4AAVI^ECrz>roCcprlg zt6Hn&-|5D!mtz%@)adPy@Q!a5UiPaw?+CQ9$3AQ;kQW~k0{qES(RG01HY zM|;{>BLYIbcxc!f3L!D3y8=WWe)fuhx2X)GdYwk5T|s|b<@R_!IFt|LkS~ONGgJt{ zHS?k@R6Om$pCOX+`Zjj#*JHfxfa~R{uxXsfEHr3AN_Eq(UxB@2QORqW7+}}qe@xQe zax|Wsc$8z@XvV*oT-#c7WK7e#&~dNNx)uTdav^*9W5Np@(0DXwH;t@G8FY4jlwhK@#qpE*VI`K?tC40_fSpZ0}x*y=*?F z?FdQ_FufD{x8iywCpQ2)+jk6hizvD#8Xc+4yV!n1AQ|5!*av+2i%%R@P%WroOz&Dci!rHGXp2f+Pjs8YBXs4LHH5Wm~^GS9#J9q?a)BM9_Sw9sv3nkR+l50$W;&IC- zYQh?cVDrUpZzNA!AgH!sTBj8o$vAEU_2bgfp@k=Cj`e)b9#}R^y)HSt5W;ozh~$N= zZL#MEb;}n3)CxbpK=>m1LxhGTei|v&gIF@9k2m(U8Ff)sGA>Qi)UWQLgyu0V+0P;8 zZ0zLR=Fkf~Jl3_Ms$0u0sTz__vQZ~cR;|uFKy{l*FTURzT!?E|mE^QbFy>E^z(ui) z0f>Kl23X#}=UzA5@OV_Q+5S*U6KXejnxp6$Kr#vQ78e!$bUvWNHs9yuTv8Y250yTZ zbSUFYY;(>5Vyp<1jNdpBo6&W}F7J;Z$2EY!TH8G~eJ`pJCkT`kF-KSx)1dVEJLX3N zNB~!01CU8ky3YY#0Vu?-o;+b~`7o6TpFX2?=Ac^Ej|afwkd~LX7(EY50f;d3o$1V9 zc2AzXr4fGdGq1#EagRsT#oXW7*=hHyWlRu~~03mB zmIEql{GsBgfL(BWgvt^3udV$9Sw4^U{SO8=pu**6xt`-7YgZ7po5(QBhH@VulDf zwWD48L938p&3HH_=amJ<>-I5BPSE;?6mD?toAX`8{&l#rqsi*24CT4fR&tNI<)@N( zWM5KAqoc;3$Fz`yT&1I(mQfxE%ln zJfhcaFC+GE?sP7+C8J*J-GMCUSkoR3Kum4n_L_3 z0Y)&ZV9VZ|)K{QyjAdHaZJWUzf?D``U5;mxizydCEA#iUs;b1IIC?jqdYg>2+}k?u zG+Wus0{krM8$VdCB?ICd3vb8%?Q>KKI|Z-L&Lhbb(PeDtM}5~jQ$CzK@5SbAE!-dk%|R|s~1oA zVdaB(G8P2#?mD0!CoOun3KcNywiMkXcQc~pwps?ATuIR7#ybK*XMt^Tletowm4Veo zMqGW(jJnI2ek{$~c-p6D^-Cgv1|LkHrQTIiA4+Vsqg481;U0R1{;So9;)3)mB2n{g z6ohK_Rv%A4E*qJS2uTEn|wC( zZzujTi_6@@Ln|MY+CUj$Z%zZZTNl6LCwZ18NH<_oIl(4+={^!9Ng9r}+?cjC@V7+K z+tVHmGv`w>m@7e~$z|?b(Hw8vPD!C^2}7^84$qu7Mpml|jyHUx6E4O46YU-&L;r$$ zWlXCFQ+MBcQ$3PCS2V2Kw-Juop_kS$Ue*o$CF~oH+qPj{5WAJnmMPxh&h+xl5}VkJ z2O8lYTvc6jEd-YL0D!mdRfLSBM}d@;B_lwx!$(^ zpdS6j`ge6LB36+vOAE(syC%w@xq(=(Z?bYD54cy< zx2Fh^5cZEpCgB(EJf}M;`A^FmS}wLwltcFvU07cq$$i#wh#&pCr~x$tAHOuz_+u~# z=li_Eqjz1AbvXSz(gJZRU)-&$fU-QDPF8f?R^Jow=r-PWJ)X9|)4@_7HsTasrFsF-PqJ=lU5DDomn-&3U%f(P>swfy;?e_ z1ADdj?t`s8x@>!`Z?C^z+{s%<$J5WRQtubKl7Gba^OfLO+n?kL*!XibF62FO!BXCc z!hjdU5DklqG~zt-@a%w=DW}*h>KgkdSIiL^djKlsKmE|A9M`mihdPGYL*VS9UMNzq z!0($!HPj7dWZ-iQwKjfgu3rlvKlXG%>4srC!qx?pmb|ayo=`zWqfljK8xHKv)>9-z z`w=sYzdADL%T4J9%pF#0qY;#Eg()i9aG{yH>;uBJCRLAHx!jm@zEvqb;#XA^Aaxm^ z@l~w!FU+!iTuHFI{+%x{JjS-_Zs;uLwrzdoI^oE(Vyn2F|L*&A zCiw=*O`2GHfJkkum1d39y}cza(kUfU=9BUXAyw!fW$6Z)CES`}OosqrU9Z zQvQ91DLzA&l{cHMrwtQaw&{gRhMnX^jkR|kp-M8wmPL-W-12hkd^YxOhOI%+eye|S z%C9V9IBiX?f4P%Z#8Ji5{Dax>O=7E zYqs^`1_trQ?;y4t=i62{W2F5ymE@eXToS{TEoQbQU_nA|7>jV|ytzqN(PNB-rO_Fx zU?^0bfFJY37a9!mn}5J0w7KRgx0DZ*U>=jyDCN$i5ax#z*-h8uPo|M>Rrnq0i|%2A z7iTm_0~S@aOl$8%VYpfh>p~i`xuhuQ$-!vDm{z)NB#P3Wd9NhHO&R?1o%2t{Ujcpz zjQEqIWI!S}ey@E|=?M9{OgbU+JO8!WJ{-1RN*=2Ajjf5=em-OBfXYnxep&;KC7PgJ z!8v9j`SjW6a*q3LkOfw}ZI`@DqNe0FmI@yBl2aURNM~EkJ(n}u`&LaC(kS-O^P=sXbY?8}q9`d4D_0P6{+tKI zxv|46)pKh@X&OLu0_@)e6{!Z`uHTzcoRsxH8}^$nT{VKjEe% zt8Nq;`f&~ShnATbkj#rrx$^~l)us$hW`)rnJ#3*hQZHgVDg?`PTcZf#lO+^aV5MY$ zdUe7P#-XMvwku4)k}dkb`e-LAU|6F4yQsk&)~I8t$uS>k=}%|v%}x>N#X4s6(sO0o z)3N)-aypq7pAPfyrrU$A2Nr3#h)7D=oQ#nr54?KdsCeD9&9U^Y87A>`p_eH;pjh0X zxc%I0r|%%i4n)SHpqLmXbw%6f3GaCQyT9inJ!xhh)>C3d<#jvjBR@$eJ|*GyB|0P6 zS%LgTev>ndGGqbdVS>_fGktEXL)mS^X-RS`oc83Ht4Qu4y_jOS6Ajwo><+>Bl%}mV z*rIA@I_3+vhf`*8?wY|$V`0tvwZi>;B38~0uP>~oDtT|g-EUBUdEWf{#r0=tHXn1BbRrD-Hs%Xz$^s~rHU`+G z-K=Q2JXA}C<>JZkr8V>HifULg;ml3W~uWN)FS>ah?^`eE_c5_6>Vdr)Bb zv|nX$XFV2nE(;Iu>bFxdJS4|#71&3?3;ihHIPt;D_|k3GJE?6qyv7VxK1bNsvF>*I zz6W45qcr-r*L1+i&b8i)l$afWf~gVc?}U^cggc1lWd7+KCuf-LlY3Fyo+aX)x)XeT*WEERqJlpVOqm%C$&(AKRP&vvLvV4<$5S=Nria&Nsn z@kp~yw`y_E%Ihgwte|_@9Jd`R;hlG@=`z3Igy0F6>AmEo2+~;1lLSAywyA$4)Lg}Z zUrqdPlkn#YDG3o-jJ&wmL42{OpRbbXw_Y+ z)%G;fy{2-#-4lcNeuiS#c1n!~SBpcUh&EzzJ~C1UU=x@j+AQ5p3y(q?Kesj8 zP8{8GNWjMIS3be>E^b;&4c9=rS_JyhD}xv(oc#Dm)GjXnT)rQLB!`61LeVZeojp3E z)PS(gb3k|>L# zd7yccpoCqw&EmiKaU5Bx?P+rE^&>mrGKW&M z-iJ&f!29I6BngD?PP~OhC7|t?a0NPxxKrgi8VrOizA`EF-G0&+_(6yrMQ41LKHaTP zg)11Icf1B|q$l6uGTI49cIx4m$ZJ{;L);rs*Rt2B-Lsh$ z?qUz&_s4}(8r%w^&UFL+BWXoZZUFbY#gS&5pBA?hji_o zqrRRYx1Gw1>U)e%wdrN2%#PN@LB?>Ye>7Q^JD=m`G`vdVerE!TO|8j`lyHo!k_{Av zS`9l17!wti1_$oCFy{PBz)IbN`;F5^nlXDeGZ`dy+C@jT=E02}++M#*L1GY`6530M z-M9-Wj@j4Hr_Kq5m`l(Chhh6%P*e3l ztY%w590a}dZVvt|tc^%G0E`s^^fF1`SQfV!rh z`k=bynv+t*h7lnky~<&|oCu+Rb~-C~fnT`OJZ{K)_ZXbK88XeX4N2dy<6)OpE#byJ zqfG(l!^I{No~&8BMhy9lAKGF`&9R2fx%h{Z5;HHRm()jnwQ{U~l8+!VBM?>EtwCPu zx5c8S{TTJpL?gQ@T@lW2>-R8_w= zhx{K%;(rqZl5-9ImxKXrrF9J_bb%HonLZO(_~wUKTA$8{vopgzYBk|;L3Mv!G2Gl6 z9wcXeP7ebZ8PC$D^+0X5%~Vx;xTrdC4my-d6x(=ER_}wlR~|-htR>;~{6Q=>eao!U zg)2-W0g*qF;ilp82m(N2030A5At`xG=1(+fE~(og^Yp{BR}3F0mQ9l&V*Ea?*&Dx% z`}lM+6<__ij}=>CSN~z2f`A8;Dh^(K7TXdN?J5KySReC;(yrZ;xzl2paNW*?{6oB$ z#;Ld+HGR%KM7V(eGCa4R-*|mgz)K@dL8=?I!TK=p3egko%iUYuEtiIsH_<=mMz9Wk zn&}!DnHjQgDZX_~x`ZN02n4@>7d4dix?QJ-gd`Q+wY+cb0aA7^AHr3_77=7v(BRQ7 z*g_1c`_*`;($jtUCiPd4@z_58Pg<53zgY5_b^|HXgw+jtqPv)6m_g0zyS%T_6LY)E zl05Nz$>RC)fi+^<`EON-4GueNoctkrIw0k;m`l5vgn@Q&m_d4!M(!mp>AFd=_1@oQ zClu$;H-5;b0R6t%xtc9Hjr!TMt-zpn#h$|*4o=(f!1i-mJ@)>VVFAD(CYpA#Jt)~G z>0)$srKwJ5nBQlZN|hJwU1+toT|x+*Kr9&`eSKe+QD^_S%z}M$ufg7s6pK@;T=MANi{ABbC<>lAEU^(Gu zMD`Yuu%KL%1QP}roWmq4Q8@cNyuYpQ^t<9TeC^L1%&9hJ$;Mz`bq z-rTxXaze>$lu6Yn)sn>$;YmlY{E#dO%Q&j7e2@B~%ahS!ovhs=T&fM9D-5wSkns!P zYc?EWPikXK3=f^~8+Cw-lFd#P{Oe2HY?iOe63)5vfjd@0Wnn+~^^CTvVQx#DGEZd4 z@sn~QDf9f!ZpzEQMj=RKj>BXtWOg%bTfFJKO)TJwC7*ok^(Qe=>0#ZID~p^%@SD9* zMVj(UW>1N%VK#)?yWD_<7kw8r)?U&7ib?xE%?dv9~g3GsXTSr(5ow!OZ$uVLq#b?Y1N zBS*uTNR1?-K4yRHL}}MjmH`rxlRBf5|J-6xIjvYfFHekb_9LqRCH|i%LdSoi2w22K zH%U-Cs4<0b9e{y42etv1*}D;U>Yxo-mr|kT+AHWPRpo1v=55qMmbQGM0q&OO`sgln zh#hMZmU87pP^czNv=1YZaJJ}ceVE2-Go+NcE*XPf^X;)DJtnO89vgyPaEhscXR#Us z2)u2VxQsMbCV3aeu=<+tCm33w-c_cLOlMe<{~BaWfgTe!nY#Kay<*TAPw3crda^2vK*bJepOSqLS^0s8Q5w1ucw6 z%6#|Rn?SHII(+sUB3pXf4~sz74qi(dYSyvUdmPvMb!=)jQDSXSG@XvJHVDwQv^5H|APJ@TOBmk++BMcCOO{L%*Z}ydK%P$^&hEW z0cC(nKKT9r%NhHxZt207{xj(;itDvLFY86q#8$62kniv`63FQS4#C&#=(B zto>E2xTascxRgn1w|cq#4Ps72{7Vq3p{)wLX8Y+{WIkS7bMq`* z{ZLOPXI9G;O#q+-_F22Thl8Nl%Ycz~ii=BMT2|U} z@{TP)o_kT)iVeSXyi1so8yDVJ9k7#?=o}!>ok>2J>%k zPM|%B)WyBo0O%jyb2qW=?Ok?2^VY==*pwll&LZ8t`#^uw(I_6>^Ea`T!y?ksvVXoD zRMG$WD3eiKR9@5qOFj;`g*Fc!WY3X#t_vJ&b~4J9dV&AkxwKd`*TIVil%{!R8nRLh zD734|(s&NsQ53K{dEkB6I9uj?y@@aZOuCz$C%C!IpB&dc|BSDEc%dU0aLS$udH@It zKv_1EU*H{p_c=KJpNWP4*F^#t*8f%zAuH+OO-}kGsP!oCD6!lWHv=&&j&){Q*~;`e z(>XI*i8qU9PMA?;OH~n1_aAqPT(^9Q+`SpY-K-Dp=o`P7xIStY{XOvMf`T{p3h(*i zx9G~wfA{hDy9qbl!SS!QzHQf&r~P|oR!`5k=S2Fv&r@1p_n2?)$uA`3O^z{$(TtTn zL}UV72D|ei+gG`rT48O~Lh8Lbv-GdD+`Kl%-PU=Fe*&3n0v27DCQ%z4G+*YBHQLad zm8YKSl{gx2@3;1@jS{?(XCXG91FPAsT6T2Zh0)9O?SEXByd3sS#r6$#Xb6i>xC8E% zBjU6FQFr#n+<=6+fvR7xpV@;Nl}JQE`JxMX6#WFjTMEvS%5Bw%dBi7#ek&95OGJvQ z7q7av{2T%I$vet-9(@%&n$>aI?&V~$qjpe2G;a_uT~s3qF;FN}v)=^i6j>VC=w4Z? z*7kZ1JYD>H7$jE>Cu@PWK6~%IZDG1Vn~YiEKNkms9WGWt#3ZH{hw^h1@?H+%s_tG~ zp`NAK58)W$9{7=$LvUwEq~0+uX@g>!$6*^fPdCLoU63;bs*$jX!yjCTo`DW#8lPb& z5AC^Yaf*RfbH7T@!^t*L(rmnQ$e%xAW_w$rcvH0qTvb>=U5u5fSQ3^jdrHk93; zF62u>Az4zB_2J43$%C-z;<9&}P-KciAc!3i@zNFcBr{~Y6|z~?r?l`qAm1-o?4#8T zDs?@g_ksSXj>E~B_m*8*k&gR+EEnWI1yZ{D)(m&@Hg(ffXi}i=h?s%Gr=@h)3(gg{ z(WqYPYHmQ<`QpW%xe&0b{#}hEzl?He$H@JDvxTxa+mUh6cZtA4j3l9WUn+;)&v1NY z&wozyF6STd{6ZJ?hbsUk+M$))3dO-k8L)wjyx$eesX zq&6R+R3cML<&cGj*)Gf46W%or=kM(+LP9h9 zM$6-beqBB-<-1QhOG$ftZ<3R8-&c`>Q%X&K;gcvR z^Y%Q5CF-P0g_4MdEd49wL!j8sZ#aaQm-9~4$WQaGnM6yGXWwDa)^B^z)@prQOaftp8=!+a>;An`x7jy#u13&BR zmyTQC4-;xzQHRV-drEL?%-grTeEYi14F}tlxZOydiVgT;hib=7<8RbEp5Jx{U6BTU zf7#eu8PXl1#}T=?q?mXa+uE0Y4W?~-Pcl{Dv?yK&OX@#ZZ}|mw)XR-RI!-nBa zRYgod1iX_&=KDrJq|g|VG=wD|O5 zU%@nk`O^C*^#1*4dCm)C6=ry=klEQdei*YaQUf;k{;-}N`DeBM_^rVjQ%dQ=8D_uJ za5dPMytf~&JiFgGC!B)bbJ&`hN&W3d(DC`GvHR_}w_v_O0vo-)U_~Z0+D)eMCuixh z#?S}np_K>HzX^BxV%E*khuW)&zenn>PiOWGa#6KyGysr?Da9Ey*zag6D6Wnp(mi%`A)xjzpP7t!!F z46)W|b3gT4TE_3*YMXH__sAlHV+6Dkw(mE*Kr)hY3>FRq%x8k^;Ou$hQbE_@TfZ%Q z2g|JtT-FY|TCV6RhyGivhk=*d;#of3#-Y(hW2Av5h15pP@p(v(I-o22^Im+v6te@w z>&-IN6T#zZ!1+@%aY-#RwB^ZMFk%u2Hj4w2sar8u14SemF`-v9L?brUTZeptc+5dj zpihjS*v#MzE$Gm{PX~{lHyHgA(Y1u$V*TFKj8fEgAWnVe!#PH*`D9$au0c|SPzTw= ziR?b2h{8x{YOZ_md*F7iP--s1`C8{d2*zIWp?+$yk&>%T22j>c~kvjd!4yv7)t@Wm87RUe}s=uvKKa+ZGfm0^G_wts@=*I1AVE$Xw54Y?5q^m;76-aDyH+z8 zKxPPh_#ZjbPcSnCT9szf`>Y&fgiyH)kC=RhgTCG%J|Sv+*fr;hyfW9RkMX(#_o9~Q zZcjRu31lNjOO9v4W_wtyB8${kWw##hw3TkGrC($4-ZcWwvq;0p(jMGtzn(T4v7wU8 zLUPffQoLf>zG)gu$^>m(Vc96*P8avn+p(Q7;j+`Ksu8 z+8`x^L3K31kBa5)V{9vGHK6tNAM6X*%KI;a@Hg051rBtS?@K!HpByB=%!<&xWz7zL zm%=7G2r558ob&r>X`lKH=I&!h``eR)EbgKs{IUO%(SsRj1ui25$O(Tozv7oSb9*w9 ztofwK6-=~`KVpb~EZ5tD(n<`65oS7uO0#0lF{%dnP@zZ!?9rmJpwN{C$Mx(G0AXnf zjtsDVmzj~*l@ix?ITAMa$%9>>8gs|6bEr5Jc|_mXa*{zNn3*vFihllxYuTRU+><@; zJ!eqLZ!1nwB)ZF1tLVbQz&b@GyjJ0VkoO)?O*iklDE0=TpCU~_K%@zZNH<`kO0NO| z1f+)0YeG{Llq$XV-h!0SnUt_=ZO>9v^d00e}Vj!7~g2JpnycCCITd_a;t*fKyt$u&Qb0om$%mU*M4)XWB}7%sqQ=^!L1Si0a}Z+pl|g zw>rzbr-Jc!wM_~iFdR=gWXj^imt8UmBZJ%31!d{`&9&9k@y%^F-UEc+2CjdCHjUWuwsRzJBWhNTaq#`~xj4Kw= ze_`MGAUdn7bfLg~IQZ8dF8$Hy5ApBi`m*$QNBh-WKFHc{ml2c9#K#|OFOM_jWVlp3 zrBAL0jD=n01Kp8A?JmqfLwT3e`J+6`K`k5W6V;PAF?Av8rS7RqpDkT5{Ptj-rp^TD z*i6?lPO&mYXo&3NyZI0&%91u@_4*8AvZ8A?h5X{G3gGm4LCBqK{F;zP{=JHDE6{i0LU{x5 z5c91Jv&V+Lf9L$26gtYLR(l;Pf3&EW-C9cTu_@@O&M=`Hb@+6Pi9(eS%Q`SD{G*D+ z&}Fe$!dLEKc77-6Mwzfh-C&`ImV+}z5@K}4nE#;!h~pQ`pHT^o9<2ravivZx!Q3l_}K+W;#N zPv4%F#T|9!(%HVqaWsz)8$VHIsy#t_UaHr08JUZl<+ctJWLFySc9xEO0(;NMAf&q} zCpuTmcG5F`LSs4h_T2maqh_V8og0_@6%Eqor(5QFLW>rTSb3aE4@PG?R|du#JQYHm z{Cg)ehUp;o?`N%kYHhwIIvOu;?ZRGA(MP276e)HmcsOZbzh0;a?($62=pt$?^=!w9 zTL(&Y?L+t0Uu@4tHCH#pR&Be;Bl3%)=G1y6$E^Wr7yo$}vtubsjQxlp+ zoP)X^A3)>3Vo3i7P^Y(& z@S#*1SV*Y!r%*cmP_M9q(o1ebvzoH(!ILN40Wp3zs<6xaJ6C)ai2|GhNc3dlcjXF= z3vj}d=L%W5Nr;>;>9TA2=Q}+$N3RT;A6|*qc!P?%OJ$Bl-f%Qq3c{q~I;WioSMa|v z1N5zE(h3gYwP-auTpg4aH=ctx7`hh^kIy0oyPzD}Y3jThzy2!Nj_by1V0)2B@U4}H zf6CF~AwX3yn{x}V?=S=gRF72--b+sW@~R`R>8s^ zPh)&>CxfrqM#PoQ-5AH31?+Q)!s?RZ>c(h)z~cp~2{axl7q*Cn1`DOsHa(LSj6LbA zGc-a#SH`>jBEb5(EcP3rb^V;uUPz-PXR3Hu`qt0d-0Blw4gm^S<^lVOY$2P+iE(wR z0uu1Jef)zgvZbZQ^B)f1+-fxwnBLK}95qW|ta{d~G`}y|JLy%M_f{9#>FSUmD*Z8U zn{G|t2$po%sT zuR}fn3&&ZUEM;#k5Hp+4<=@Nc`$>GexE-chxc9OKtE38>MiAz3L(XA;J)YhKLyvP4 zVU;qcs*03L!h40(?D^Zq{Er1KE1Z|Pc@AdGeK!MA?hNncdfL{R^s(5x@X44}JIH*blIT)FB;cvWQL*EG3Z zj9)InFZ-J9lR@wQGawyh(dyNyLDEbz0uM;RnJ|fDwP<~e33xyQ`M(+v_|I~?NW8%d z%A5a;VF&FCK8ed;%TnlQ9>~kdc@6S`HPqD$0L1SgKzhxj1hRAI&y(bzz&|nh`5HT+ z#+L3HpQ?7>1Gt>YA@jlv6wgU@i;A;SA2N`8BZbWCB)~Hm+{*#92UDGiH zkjU#5%PF^S9yA%AChrh$pV&WkQlOGoV`u6gc3i3?sFr`l1Yc7uRStsi-kX5;V5{AJ zXc`n&ANAMDeEv%!;TC7QrEHbdThCG{`kkb$`?`2Xttke+5(F9y$c>rL!5zaT%cD8) zs=|TkBv(lqZk8!3f(9r&S;PjA4YRMLzJ0YTTEG3OU(=#-1s)_i4RCxorkpVP52Mgb zZO3Ts|70c9cMpCRZw}4ME+2K7CR*W%_fv<4tt-q%9KPbV=TL$auNI%+f6A-$CSBWJ z+d7UL3|dL~5zjhGZ(n}0|H+JRKMG}8nVsN~_}E71MK05_a-3ObmfYG04pMJ)$NVZbTnOFDsH4l>rxM5j9s6~EzKbIJ7zI@i{+blM|~~(%5!@k@h7~q>$)yE zZZu~Br%4x#+_9>8Ys%4y(^>aD3y0rGH`n!j^oj|#gL8f<57UNMiE8M;|0-}^Um3F` z*l`gfxXFdotgHaxmX^(E>}an)|)52x<6#iXwG~( zolVEyYrO5kvE1P3?ql+r}tGKx}j@G;|QZ+$w#Bj zewGj%qyr16uUQoio~Jt7zIkQLY{qj1_weOmtyyx;RvfU>KOEQgD6~RgTAy=-hcxIJ zn^nWU*ZnsT`dIA>p2A#~E&3o8)d+I_okDR4ewW=W2Stu%{v5)A)vO3 z0#p&S2oNd-_4@BFuS!=!#V;cxsRRy1{vD3IUkKusg^(J zo_Iy~+LFoP;o}GK%|Xx%k)x*0X2Nf)17QRG))pdQt3dCPCO&r*Xy=Rl7w7b^YMmV0 zjVX}oWBH$Q%svg|Lx6baKOvdT#5o^gp&y0dM!fZJas#9m2DZlH_)C*nMn2-H&DRd~ zP#={k-0iCOh1v)6IJ;mb)%n7PF`9vnz6<@5tdVp*x_)7UvY-1NXM0sT)Zh~oSfM6i zr0ltS_hu^Fkol6TaZ^ztVIoQNc-dma_kouD5nFP`8O9oz%<5uqBvuuL4|yWbT%EdT3;>dn}g!bsWPM(S*C7Pk!5Y3lhd1~s&;jQ>=N zTv2s!;a1#R5THKSYfdbV;FZ3MTy?;7c>4eRljq<-LBU(c%ZS>%P1yI$;-$-tyuRWA zC}|P*DPo3|Xak_FStZzZw~?ldJ=CJ|n3J>D_@C$NNl4>ITBnando2oJ60$wM$)%*W z)(M^Qq)Z@@*q&^w5h5}%@T#JBdxS?$zvJK3HxV9~-_*jSzhEvRrbtM%OAt*Y2!pg@R;+XH`r>Ek1@C}W?2W2F zDDp40Y=&$_f(Yrl--*A7hxKt@Awdn()YIYHuY&i!V#!xW5tGU3;0x?U_HStxZn;le$Ii$k9R{+B^r1Wb3|uNJ7DjC5 z?IjCWIaa68ECokzQj26MaRy`wy+gYsR#-svB;$ zzW39^{&4lUZ4zC#6AG@6m=r~Ng^UEYCQQA4xEI^&M7~^C4#O$7eQ%{0r=BiGIM_5D z=)`rkdOXS6un!rH-y}4-Ce{`;6enW4(hi2W50c_{Y(~I*%2v3<^B1Rsgbo9ful|`e z+UrNzu;vX(#Pe6&u9^Z0v5b)hFSMPI8XK787oMJ)Gz`L!D^6WWfcL^uWxJSRMkJn9*zU^xJ9{y_3_nQY=NiSs8 z1o%pmQ(HwqCl=A!D?2z?t@oYu4|o6}^hfAvh(>+-p@L$Zux)PkTTre(h>Ij{PW~3z zG41UCz6SaHiR)Q>s6`zwtAhL|&?7V6UjLrnT4vVvOWPRc2HMZ4ZT%$=%nJ*Ct*ZCQF#FPX zDzmquUw?mCv{;>%F!|kXy%-l4me=8!>)@U+HB}l}4bjczob)C1Dc80jWF0=$}N+)FZeQ?NoB*^di$<%p5PN_Y8ZA=iOo9g2u|86dZ;;?>K)I zcy>TFT03P&cdZ!5f9SZ@m%DJ71{<;j--0Vw_wkb-$SbhjP8N~p2}>fDd1y3oxo@kC z{IsBp(HAXn`g*|dok4p3b53t+)8oqxGH8uOe$&yLT~!%rHZmif{*+ zQsmHTZO59!Rb#s|h*v9{?hpkYa!4C!C=BxqO4(c=1ucj$da~0io0(VJ1s-E&RcZLz zL2~tZuo|?Vm%-omV~^?jLcwixj(JWgmWYG8IbT20GX%{~HNE{gfYL?LS@T<$Vx;=+ zrrlN1;NFBER$YSU4Ypi9oguR|F&cz5wnB4D_0Lcb?3|-Z_^a!o8CYvo#l>yMF#^_5 z?OT|J>cG8qFb#}m2c}78O&UwS5YTL%amVgs;jUoTY0%mY9`f){E|;wlSPhas)c>6! znRMg-GdW=H-zSCsq8fOj5?$m<}v zk#7%l#=CvMLeusa31!L%n#OVD?VYV_a4KK1TE1QGesBf`T1lIKo-*f(w zWrL1H87N#?FV(;0^XU*a7Aay1(L3VRY<(jL1ggqe;ED<6#0<$u5va^FlH>|3cA zNSYO5)k?AXPs6tA%5R$&#lq_QD|An?ikExQEA!b2UB|ap`}rZQb3SEP>1x~y%a1ot z&G71G9r;3r1?811uCK~b@(d0x1Bf--k%#fI<|=&>ySRY`=OWsA8BvHN&_b&3=ovP( z&L2N?;3mDL`=lvPL0yvT&a>v0vOd=IRgD;{%lp%%adt><^a{UX$jF`fY{w%O*@e8^ z_SRgad`_GcoB32_eVFs2mxSE@)Qh9ed)rAO1~+VMGG~QG@~es@P`w3lC?cUSbU7;* zfmD4T=ZEe+;&06@Ew|Xi^)eDR(MLgi2*=+v5K$99(FaGW_$0WEHLz*#fKQ}XB$?~R zkBuI@CJ@%o@nazu=24hxQLKOm@4CqYne(F6Rgyw?I#<;FO+|)t^i`Vg(enRgFP^ZgWHOT)gbi4b)w0?io0#gr|t9m;C zz|2;1=)^K@U+F-nv-QDX_6aWK%&UFGLYy@E{<5BPyFAvg>pAz)&Ux9yEX&262it{{ zr3)+TmW=l-mI*i`@?K7SnW8Eo9wl_M`YgJ4h&Y>90}oKcJc7 z|0QbXHVeben|B$;Q|F7KcR2E2J=l(lcw!xR%LbBiq#Hfo!?#+U1dqz#*kHo5k?_Ty9*Lb(B4;hr`uYDhm zE;4W$$@*t8fqmKqed|hR%j2KqK|v*)Zr^XipDgn#Xl*>kI&_e*{LokE zb|lF~sk1haV*ki*lWXqt=p|d>TJb-c>!WM;?|28y{4=Ezf#uzzs>?RGAJ8QSs}ooG z<$G3xDKkg`qO$`Jc%r4x5^HP!E+9v(pCjdZM4%di;!zDA<+^?$jNMT#Nh(E`;xltv zw`Hh7WE=E_6|k;Osje1%6F+;%sgfuPBu4v=3#(led-DXQZ^GhINsRVr2{3CC_x+QO z;CymrwkrltZc2`&Ru(|mvj0u}vPOVal~|HtC$GI_zU3w*G@087%Z zfzCe@!8*%*l$5-T(o%9-I16C1leA=#dut;AnL|KX1uM~+M3HpSckcjv_fRNb@b6Vv zquC+i`#ssq@DZ79pGB^Rcf7v7hM6tpCA)Y0VvppPJE<=wFF)Qtq+QRIl%ud{Pp<1EM9S6>%jqab#*IL9vVEy zvaXhY4-_>d=`DB)6Ua8so&{Q zjVMHl!!$~t-90$yhd}_H*Zp)8iUcJ_7mc=q+rcycx3>QPyCuL^rPw^e4SDN-C|a(1 z30khos@a*!@jTH4=FffO8Es?a;%{;IE{9~SZyFsXHWpc?8|rUve*Z~s9Wt0hl%@AI zDJ<{`B=C|^tC-GSm#hd19eC~fNNwJ-Fy^8$Gbjl!)ju~_y`bl{*4qGi`1cTXPC|55 zd^8NVZs0&QR`wZSylrtqO~f=+(!Un`%rUILJ_K57%z zb&Ee@U&WWDt}S6Pn*-wp1qF`I-K@*bm;d)b-bPgU3)<@*k*-PxY-7FZ<&azq!H0D! zawr{LV43#;xzDveDUT-ohNP5}P@!5$7Y>Tr>+k_gTz2^E=Q0cz zs0$6^9V;#`ar@JwCzL|TVV!lI7qzYhf6__=g4(hsrZUS$^3O#1Us&|;C5>5tx!ds zgANc+gM*D5YQ=_e_oR9(#N?ZDd3nj9nr9&9j~m^?8tF(fKbJF)Tq6Kk%ZK{Uv1N z9X%=jqjiJmSj%{s%)RHAd}fWG1^ru7Mioch=Op|e{!;n8cU=F&tP%a`Pw@(n4p7z{ z0nl%ib7h)G9qRvdwuyj$;>y-8&1&y~{x>1!zN{Y%GEN7ttP!;UE>;C%S!CjAKwl2{ zu-Pdcz=Hti08j*Qv41)D_3P_*?@s;OIh2-`n;B+z*-%3~(xrUM|ed6o0t3%88mcYqKs0LA|3lDT_>Cy?t=<7jmL){SQ*e*df}Ye$w&5zmvuq z7c*?cCjLbKGAafkke^*$U5l*0HjCojsZbj=S7OUiaUREU#V9)e#1n0yW}SD&iMms(H7rYNx0> ztedcqIOxzwJ?^g3t)VtKJ#}=lPhPngyLP^ST~C!kZij}Nzz|x0AuU%Y(!`onT2j6W z)0aXGwJ14bY74OZ*C}?7W9idlHaR%0wB{WyLK7F=IVHv0n{YfiJLO^W*q*xEsNumk!o;$ZH3O=Oit@Pj@ap{;>4Tz>`-`Z8%67c%1UvK?^Zo8MWE6^zO2ZY zlRgQ5RE0b7RE3PXiu*4V`lSudGc;`^#CAqJxhom^c~~d*E(x(?36tg$@C-d3;RD!O z28VUQ7JxJ4iev%Yp?xRFnd#NM{mT-fH~}#DJ{1ha?QV&h$mwI7o1BuSq2pZu+(SS5 zURR{flMt493e?azba6<12O$!IjCnln#L!lQSy@FMvK&>VFA~h-4_s@#G)04)mf1OR zYYtIBDNU2RWMuVSxZT05$!6DxFi-cBpy~-mchEZ{{$X`nr1%-g7`IS#&jYC;+M1qKj?9ZgJ*C4EOTEc$gM-fJ-Q391qI4TRn>%V2&8|xlmPdU&!@HneX)QFjFL*i;%hOu zuHJmHDJm{cOF*9c3A5X~$JJYOmdB?voezD^b7|zREL$n{41VyOfoA9NA9nz=A16h} z`A1T>F7XsjXb(I#$*E|eJuN%pJ(hV&UN!rr%j*tJ)9Ny(dgS}B1oR__G;OS^DWY67 zh-dj!(A>CE;uGD++=*|kfLT4_@4G9>{m3#}Kd}T?I$lzOw)BUABziy(&xEJTbuqJu zD_(CA^#NT^ueDxXH?pup?s-}TfVv)*H7{IJ`Wo^dk*)-(RSJd z{=U)y&iVui-iY+AgPYlvX9>ijCw^f-9mZ6miHWB-A~cDVd(v>S0;54y?)Ew;DhzAP z80X5&cijdtC@RtlkKOwBLnmC6Dn%*3A4t?)VU#2gx)?G8<%!A*rFN9b9l< zuUVEt8lF!69dD#CvY!7hy~Vnrg9IP-SuU+L-Fg}BD5bte4u^nO_YGsPPCUz*yGbQV>4|#wATwim6H2J-E|7o%HF!kW+ugriY+A` zD)*j-fA)W{d4Kn#WvK9|h~s!)`|8rR%hgDs05zz3o~l;noN|?f*cc1zbF@J75rd*e z-htb4UYibFm>0}A=t=N`w=l)0*UN5x8xz@T1ACI#FZkf~g)(^VAN@P+$ps0A7@fP2 zRf~f>UyWTW^FMf1eVJp;ipxcp+nMZXa(-94)S!DWisHBX8b9bE1_pRFRv1mD#ge1t zd<{nwG0dGSh7FsKn!4y(I;H?(J3`n)#Ocr=BBrN|6JNCHO}5uUt6p<9yDnvRozZej zCcUR2+orFx{$n3+XIJLviQb&NoZdM#pe>T}YF6TYUXfA3q?IG3)sN%tk6V07A9K_xE_1K=XIts8RC&wq5=H z%jCO%GR*;F*Z+E7!DQRrpqGE}s2}++#xLm{YFA(G2!h4Hn;b_y56-fg#lkS};HCFQ z#-!7=W@1Q_LEF!u^1EL+C|r*P>Z;}h>O?kA9qnu(6qtHnT)H9HZfdRLqj(#adUWCH zLNpiq*62PyMch)z!oi|v-3RA1;qCb(&c=$g8qon&YV>=(jmQ5hq$ON;G)#>U<5QPD z;gfVw?S#V=Hz#G~BoZ=Obz)##_UhwSkUQ~wuPbQsycovvy}(9(H^HPTMUWIGGt_iS zex#TfnU1D+scEyq9kvl5W`r?EUiH^c7gAjr znDs$!4^LBVlX;=mA7pT^ciugM^(do{kM$t?YmXR$5l{2WyEJTM7x=Zt0SmXZ8u&j3 zVj280t*F)su3FP3WFe(#HAJN%TL=m9sXLjO-!B1rDti1Hbw19p^)FiJQDu;&?ZjKS!S+GQ!jS<6_gxJzbF*b!#$ci&W*!oN5+k> z0mp2`Mzz!}gi3+~<5i~w^TcuIxs>|o{%Qs8`R-@%X!SJ=Yi6nO$)p~YOxuIdxMMri zSgnMdZqiD+b0LZNhmmb`_NY-%MLO00sErY3_ zSGLf`a*@2Bx2H!X?9o;L(zP;UwkH#Udvu-!>%~OTi-hSOv7d3lPja;ooF_KkC_Ry%qUx`H_jR6Pgp@g&vAvcJ!0YR`i0n_Y1H zaZey9cx!7+aCOsLlv{6;$yMHAaAk1PBVNF4kd9#oNF)_tH@)<;c;@GE+aYF;CmG@$ z2|jX;+b^+^8LJZ5meoPnbVZiNfCiD0BJy~^X3JJcQE^V;;`kY>tU(o2H(EzWXo6M+39IQc| zgh+aADJ=j-$@))NesAW$2=({SBFy(M+I9i|abWPdf{+qvNcl;M<7)|Vd@&=f)x7E4 zrS{&xe-!R^ikb=h0A*JMla;@oX@>#0s{p`UTwGk5qy_LFOi&qYlL}}cq}@76Y>tP`G7XcG9L7@h~NM~6G7#)B~_zOUGe{{dm34q84 z4ghWOp??ME06~!oTEqzX1!^k}*1bxAxxxvy7yVaypTEDqMz&%nCWbT`#i?a0k^x$! za-#n$xtkNKHY8De?~sdYL}D%$@fRr9SzkTSv37Y4YNCOyf@l7($5zq>q&<71i#l$C zO)OishI8uJJ1Jm8zdUdg!>c5~gD%rFj$Y5=QH0Jk_AX$!4g7EhX*e?gkeJvog%>h{ zomkQi99{t?+E*!hUQ-=wfD92Gwc>qeFRSN=ACyg|-;A7{0$4|%mfdyzLhywc&`=X? ze_b#7!C06HL6W~C<*IPZ{AY?n|CH^~<`!G?KVt(AWB|lQsOSXMSiYnZB4+ga$RIx) zcaYpEIy^XvpM#FU9W z)3hN2jvj6LTAKoV&@)9&od<#*4F!b*>2Q=_#BlWXeY^le_#%Og`Oypf2)1J}2E6-^ z!mG$?xqsgUQBNsMC_?Sw?2uG$NJ&1-)maPOK04`!64;DqXoJIY9N1S#P0Yhq>E}o^ zFYG0c%^uBWNt~hjP*7H+mbu&Owi78d2UIQ%CUIe zgr>R$pl&6Stu)W=eR_>+vwfcLSczAdIrlw+wrb1DXkO^*-eH96ous#sUTAhd$`3h? zS=7m7UY&&@fq^Ivvzze>dNu5GF_$=IcxxymE=IbH=G)7cAK?mxyNJzkn){`|6i}r6 z3$60-Sh)%T4Kbz5Ryl(3crKpO$1ym?sv2Iv_e)FVFIW~yo6{wSR8biBx0GHhaORsi zE8qi;d@D5!#7Tne`X<^De*O)xEm|4%$R8w-c7~kFg<&zN_gZXhX!hKvpS?vvtvNEy zWfVK26{YqcSL_oSCvH~cM z+2kfk5fN3=H3NXb=;Lp0%*dx)&&3cv@O9Z2?D(*J>;sItG7Pv1=sU%>C~n{=$HCI9TQZPZCCgA82M;)-UI|LNau=G}LOwj&kT;i@=nOKi0Z zheEcxwSK@S^|DSX79~?Iy+W!Rsh2}~Ea4L(6<3@eX$itU(Z#d}VX--ep9;#Mi5t8E zb|@IWblCPd9*vL64rgbJUyiuhnB^AsKS{KH59hLGniS__c1|L7c z-nJ`+y|#PnD|l#@Y*mt%IzT&;*Pg4kLiki@X-*+XPU-B`# zRqq#Hl2-S9>5t5P!eyfwBX8ipp+ft&HuuDm`NdCCyqH@-*a1Zf3~SxQj|E`}>eG79 zD*xs=&WMX4Rgn#LM3$Q8aldcBXP^bfNKwQhTBU?+4-P^p&)ZrMN=xy*BTWsU*;Reb z?7Mhbvd|Hsc@YqbTHi0xB2p$C+l#EI)M3;nANhRwvI0-yoqR56mkfP!a{2OTw!G~1 z>k20La7$OSZP{goVVjE^Q~{sNEk&(;KFLVpYa0w=k@?PrK)3ccf>G?36N<_MgeZ9_ zI}jARG^Tap7iMXLHG1y1jPdKXKfL1fkzX`AHWmlrFSQdiBO+!x)@~2$R3Ue2PA9o( znr(V=ivk|q{cdIxe2iJ0U>6EL5`u3F)l5+tPORYcp=UUScP zIJdKP8LhWTY`29!A*eNL7qxd7S;w(KHsb|wqm9FWpgG(bO63U(#pOIkbd1c&)h@d9 zhX9yfQ1T2kW=eVPXJqxiz?@8G_;{WD4pMNBm*yEQnfn0n9mKl=_L?B|3NkrfSo9qJv-9q0dcEy$j9gOByLP z^3I8h2bu<6EozctQUF2hM zfM!PVDQGlUe_8N-tOdo}nvKRD;XJ;+ylw|1P*~Kr7X{VB=ji((%&KzuLZDHgTNKtP zia8Rpyk2NamD`;-<3ODE^2?mtU0LYmRSjAIm&0!K&)_EPv83;LcPh+LwbkDLCXGj- z&WE$P)}y|cVGcez5eNDjq#`!%ZFHtUp(GKZL`8i1QTZJ`2K|p^LuEjb@G$Sh`dG>$ zds$zDO*(i!#vmt7I^t?@)Lj91mTkjnhR*esVw8W{a{?bDelxxI@Hy43+4~ZqQJH-d z@!TzCMN&bi4ISS6RF*g)^`39b`$ntRXOJObAki5tXKsL3Q+|Zs`c=KXza>`ViJ!iD zR34h^*=0)FHXG~=s$!Xy|L_jfu3|jJoIz!wL86CU_v+8t%kkM=zhx<2^rv*BCu|RV zO;uNayHJdJzr?V(4W5NdaEq)Si%q$Of+j>8+z}{Aa_UViD@trl-s+KBhRlPo-th_k z0LU7&y*xSbDmDx-dQVGp=L>7Dg?#$-gxy5MptPbz5aMxr;B!b8^Na$U2p6ZDT5e+h z^G(y**p38wFQZJgT_;;QTIrUa`7c>nxp%&#T;h69FW4ZlGxqiM`?UA|^~oWvh@aIc zSYh2@edy6_^&pyM&U2h0Vb7C2UVOZ*LCJgs{fYFE(Svc%CGwMKc}PA4~ii~b(Vf0H&&G-0}fcFod$rqtxzN$ZU* zkGm1GX2HcL*=f{WHxrRGCux|NgAfMKd#`_9V`d(RblB~Ar-tY%p3T?)Qk zgnOYICiN44ORLxszng1s=J_gb>j!^uVj;motWba0+uv z*)}0vgRoNlX`Rl*#X+gUkMf(I=L`Hjwzq^BCHx{@=Axqvl8~-YYaU=+&;ml^(M1HmrJ` zZ-o5z^qg1wLCn_p;%#R>S$%F(lchl8(`M}`vvmmX0`;( z6PX>iD?B>=Q~Ore|9o-H(-H*f)Yx7kA08*DPGRE&>yy0MT?EBD zCy%tDYjxBoS;70YYTCCCC%+o+UD83XH}`qQy+9I6TvnN{GPb2Qma^&99EXV&lC6wf zzZO`8=e)Wq)>wZ=S|rY98sj|aNV%C_qK0eJJ064X&U0KKGR-!IunXgVTUSdte(w?x z!hQ_DE$;5$@@(0Bfc&d#Uq@2&@fmgt$1X_OSA@Nm44F&9$!DJnlnxV(hMpZF0SBYZ zW!60zMhD?lnCX+rhZnlWpZPVK-Xt71t^}T0lCX_oQi`zGIvDTXV|>uG{%bj0_q6PCmu4y>pqt@9~T{Br3e}z^%SEpPHXnx;pRK@ADk(+#FVMc1hsZNRXZma4S zrp5GDK8Kf8_O(LYJL$H%VSnEF_0a09l>2`hvQf(aFa9^7Y5z@gMThf2(Qa`Y!p+Ss zve~)Px^X5nImVa2E#LO2DmgGJ*4LZ_8!AG`P)drhC#M3rzG z0!uvnLiOivn%GaYZYGq*a$DMmv0m8Vd{joc1o^1Zp4BbZIHdTu-{_#5?S>)vU{Xw{ zDFn`O@_*LVc)FUq`f!)}X^*EMk3o~~Xw}W(MqEeoPr@|K*g%DSya&B@iTf3#XUV&o zNW~Y~o4C5%A4)^yN7)baizLW~e_i@fj$5dh(7HK922;K}%`Vwj z{DyV?hF~~+h{b5@`M!w;vYGDrK}y~3)3F@j|OT5+uqoTkp_ zvW!C9ulJ+Dlf&hs0kkJOrdP{8z3Hb{i(#aGc1P=xqnHm`cXjHctTFA#u+LINb711v ztNS5l2rHV-O=9Ur>GF_wnx3q@9FBrAc%p9a#^eyxJiEjAs`qex%leYrg`K{#RvFVyFTeR0c;ikSrXzB`yM$Ve zK$&*qDdE{&aDthVhOw1X%uVofK4nkYL7#lAKqz0x`7gvyu6sv@LXiwTdsaGi$%@AJ z9+`Zd=9@(c_&Wb_5uv(WJU1fpcb)lRA zSvit-r&}pmSo)I(_0IF{K9i=jUp~9(>i7WXTbAuIj@o_GM7MXFPw&0OD4a}%`RSPK z#!oh<7w08YJwADfl1n7NL69#i;7jCHU1(4u)(n!+@rQ@czklA-qxtgd8xvVRwRmTO zUS*KS+WLCS!qs;nEqt}o$V2y9i&5iiAs0?*i@dGTUEf&$ex z#|>?}9`9-dUg`S%lW(G1YRbsybMR+2#FpsQ(}L3it)BJjd@UgQq4w1fWbpD9KQuSZ-dFWu?d zTYIXMcDnJw9kQQ<3BxAkg%bA-#Z%8to&D?SqGltdpRmygWw6bVhY^5yXOk!nEPwE@=KYwR1TZ+?!qEDghC#3I^5^k=*uX`@6>d*OmTg zF?bnLG&qAOb!~-iZ;4T=BT)GKF-x*S0O1iY^Jj`-X7YrGkug_;?}@8S$BI7=oR#i- zy=KZ1PQ5x?HgM&l@f+yfcjZk5iHR~($#tI)8TMIA`PYY#Pt^APzEqgdhD$dnj>IQ_ z?Hf(jaTysK4L3`7FZQSj{ZWPgxw-tSRONW6UbpqP9ly5TQ0z}GNL%7OQJ&u2zBg8A zTIsN6DsEbiAwBz1vDZa6UCmG$iSXEDLO06mc4>N3hk~TFFr|x&Tsz9b+>g|PP&3pQ za%CM&Uq58id?@Tqh2LMH`1Kv-b05vt*Va;Uwcvh+r0aeLy(RxeM)3FNLmU2U|kP*Zl*gtU_^Tqrf}`9@XR zrjz*Z>iT&)lhE_C&b3m*IbbSIZ6#P6_c&FBmGa5RXwiWPY5bGfZfkGn!@)q1(tOHrI?5!O0)8*y} zxX8glI`K(8rzoWW7V&rIe@TAx|GocZypu6|CV#&NWAcsH!NWri{PNfQ?3C`y{qL`* z!X0edHVdAV2V_1d7(GZSUq;Dbq^76`*W{PwSgNzCMZY8_*lHX!;Fbe>sqcMBFy^HK z#=Q+HDRbt_tBnkkhY03;P2RTyZJcFo_MVeP1Z4o(`J}py-hblET)Mv>A5ipjhH=_b zLqxD{knHSEVoTnTt;2e=z2ILG7OcaV0UaB5&{Ck8d#}B_SQ+`2obhMWClCzj|P2#Nhm2W-*;!4KQ(VVR zd_~v_H3{m8@eN*wb%9p~H0`wo`W5E_7lmG&aCR&#t`4nUmF|0LMvF1bTVLEvoT)Rf6AxdDw2J{mDTWVx zEyG!IRg*Ma>c}OGek0|ZH(1Bn!mc76ElJTUMp2=&PzQ#Zb@z)4%?{7v_kMmAhe?sq z8#(vpdGW%qO^p@V*DIN$woq4S2iOwN@rhkxqS#Dm)!a)U73z|{J;SRR#Ds5NhzMyG z4sbG#FMUK43^)+@ILH}UPvV7y{yPqe2?GsmBiMT=C0`JO22RJgiYnq z%_YQW4ewiZYQllJ-=b??y%vj<7?YXD*GZ2k%=?(2vb-GuTfUXn=yyj)+VP|4wu6-^ zV$AwBe{D+(@#ALIU6FTHANmkSroGh^#+nmSvwyV~)E~cPXMY$i{&EF6>+Vjd(r`Ow zAPhd>PBjwSw9}}y#t^L|J?IulT~x4&U9opck;!2l&0Y0|+l8l@jXLi>A>dV87j<+5 zb>ahF2`$I^!=yJ$+pY6g|?|H9zpYhfD#n|r6ODY{mW_qv>_akkHZF|qvmEsP;FEH*=$ z26EjKzh7FNbv?>+cWM)+sW^nshan_sd_avRd4QJ7rOh(Ip?0U*V=2Z>y9NjkU%!r zW4wntxpUz1><;7LoAbaHKRYv+32I4J4mj*%N)Hs6SjP;m?s_phwdIbvA`i%f zq2G|66l9RSD)H3#WbLT4C?vW&<(z;q-3N0!g2ddMGSugT#LJ2f1z%B9k2Jpl<}e>T z3jP8D-E=NsLZ~wLW`VK&!6X}mVCOWF0dJ^-pJ!l28W&tx zgwD@46gjDyL(14Mc^SA#i43f4eH=)Y*PS($`iJHZi*&R1WEl7{sq6gk7C7ThHKExD zr`#IhTK>#WcXUHoDOROmhw$b`casJ%!*q63>Ugk6oItEhSe@UMO8)f)k>-kOr5)8W zzkXntZ6B?>0Ds!V2hKZW5#YfpVn8f)NF!=GSV_FRWUR_6Pjc~-63lB-6t4aHhbnhI zQDNU=U;zz?1`cpQt2v8lXqoONWh{DJM9tt&bBVXP+b_MiRqHqLCLO0-S($K~oi}w| zni2lN@Ia=x3U;X&W(>z8BHC5$p6))E_N!!E-d%tEDg}Fosx05L<3wq=&h0l03yaQ2B zJ?~M`3|R&b6aLIY&~k|@fmPLt6R~6<{TKz==ywQ4XE9{sT~&d*EaQqqcU3obV@~o8 zLUb#%(o}~b6m4epXP=#RU!obk%bzRWp=i}yA;SC0YwyIoJdV@wY_qM|intG4B@G+T zvZ#lju(4pRLTs>1A=o!5Nqe3NERfPekA`N4R7!)9nW3!RCxDwXzfXws-AJoW-q92z zL+PJ0eY8{E2@_91IhMHZOD5-}fF}MZMVBL;F=B1BHaBBHNy{9(zW(^!!Ec>pfw2<~ z48vT~9IezE1!&O6!_aqc z_RV9>B^=(XP}|mr_gC+;QL4o3A;O_@tzZt0>Yai~>dA3nfpb(I&~OLdkEEc<8wC)W zU3zVKg7}lKjcJbj-19&MyBNQu!?b+!cgO)?dc8y>`O2U8cdA62a2_ynQ93v#wc}>0b!B`u9I)VB&!i$s~ThRDN|udsr~d~S%dGf zbAmb$`e^XNEyl17UxRfPO|dsrfzBd{^dpWoSFA`|bJRB8EK$w$4>g*rQ}1HVFBq7G z#!G@i>G|kmGeC4@Y+1j`*gW~A8F;rd#p%nAXIp#tP-aW}`<%z2;gZ3Uj`N>3zVa~| z^HA~wKPVoc>)@d&co$cZ6Bt6Y|3|y+gGi+a?iQvtm5VBMH)lUR<3FHt_D2_Oy`X2> zBulg0Io2@pc0yy~p6Gqwn4{A{%#(T`ARbc+o3($rC#9!2V&!CmdeEOH(bw=e`}ROX zQ4ZOVxFESUXv|yZIxMxz^+=HC;=!`msA^cmt)4n+F(k3;ba)wcW7lZGbwEurd>`&q z$B=Nmz+cbZ$nPb5V7&fuG9(9T;ldvA*z}V<2@&j6O+Ro;XFVBk3+XOTs79e)=^VW< zs$=+rh#UjDEHib{3`aS8J~tsX&iBpeE4$S5#8OZ#T>4_KOz#6=GpE#FLy_A+;pL3~ zWnSCi2}Uk#Xrba=FRg<(vs;o_5Ao)y3>iut>MG$(%IDH~sM9`&Hif4ts^G^v(|JQ8 zkia&!THW*s-VkqMAWEC*-m25C$MSn+dKmJ#rCuBB=H=~6F+mcsox0gk{YxZ1;&V4D zFgbV8?v35vG%W3xupQMDAzoCw(9$?e!}@VybB{m4!az$L(%bJP9Ua-UajAg2OARfrb?}gBDNQ(lCGT#Vhy4uPQLpBDATr~gX1p+j*0T7=n!h_pC z5Ov*60)Z1Ee@dQeh^VZ?EM1Y$TfA=bda3Nzkd}u0^gODT%@-xq3!QJcn|vg8kdBuo ziiVgp;RGhQA&zujetyT9P1(D?q#{Y{uJ1E2zqS13{lXwpsL3{lFy&s~hcaF3K4ZeiWLI^lQ7B;sslA3=Q16h?cy z3A7%O(ifZi9NfZax>#=6KgX@~olmasS02qiM7OV9(kdI<4&OJ_wzF0?2?MVOVg-yIzW(PKob*xeeyk2P&#A9p zfxX>XUtd@HFSq1>k93= z{dy;o4ReQ`(|=eiAYcp`SQLMMAqak4H*dxKw@H3~fz*))=Xg+C-t8)hTB%d)?@w9e z{MTU&%VSoS_pO+V@PK7RF;75X?hRCjo$xPo8~YuJ$t55FK(=gTld)g8K+M|L@psfY zY`fWFgpzQw5l&+K=Y*saV#)x+Ze8>Oaecq>j0aO-GhIO)io%XgpA`rq_~7F;tj2@* zx{TwMJ&1!SR$JucBnztiP+9HV_`{b4FysycdbU}=;|KxN%!yKkm;%O#>*nP?EUP8D zIFGAavmvfgTR^aoqwE{6L#@I;aBJ6(I1rzv1Wi7^ynH^iRt^!-G?>m!;SGwVCJQK~ zo5v#H=h`0x;~$mx^w45WaL^b&pVVpRPe~cpu_C!S2bxnqRk}QjH+i#r=@^KTm_m4Xp?#p?xM-%2{GW( zv1LTCdYB=uyoAge=@k_rBV}xwO@A{4<3ItWQ%&9K^0HhOke5_{4*{CYRENY);cpk3 zKH=E^4BP>*wZH$M_P^m?bK#nI;asJnme!Ge%-Qkr@!Q~eE8z8NqjRu2shge7Sm~Lx z*iv0_=4oRKBvp4e3`&92&-2?`@d)^<%YK^H3P7rjZ?C0{w~|PCFX~uB0#6;HWAMuy zd&S*u|Ix~I_)SAaSY-DcxQ^;6q#J)B>#5#Eh!85tg)=??TG_0{iS(5_ajlB#>Bjd? zCZ8uWzi!tn)hSiA0fY0)knkS>F)m^xh^~#0XocH-=EKyIHE5a8*c^&lE*%aD+7mfA zmwo;AHCuI~$5Xq@BT*rSr&1}8KV^ebd-}$SldYy>T7ay=0jbzYT2=Yd1-s+`n>1<7GYJ5k0x#4Eiah4%oEkRv-rl&I}s^wadn7Xh-myW|8`_4ej8pb+R z1u|L)JDD5xq?WW69j#NR6}%49>6r!63_XT>uI4;?3xBC4fLyV`W_(RK+nS3qbiT!N zjh4WO4Ky1ip7iaX{(@wd#LalxkvA(6QZJ&kNM!8+hNDdkujws{3Z*^3Y-H+3O7K@D z@D}sUV5L;VpSq1U_S#8kUh;|P>jhHW^GQ4MUM#uvt!bqH92>h^!XGG?M^%|$U`#gl zB*eO^le{T48#jbf=)0r2D$}vq$FCA?&>j@?g#S5~)aHz`rO_vc3*0(vWE(uvnH7rL zj2~v|XWBD^N`lY5#R36Nt^gV$6NA07eyw+XV@&q*?m0u7?WQn`uaFk8tjCj`6QW#o_#@ z%*(2SK67((|Nc=fjcsylU@JO&YQzocOKvACOJ8T|nPh>@E?$2u=c?5?+dYBFn1G`bj+<@ovS8$xWt zL56i}S)xiCs(igR^0t+{BI=x1(O$(eYYp6JxC0&&je<$C1Ftmri3?=U^_G(Gh)=pS z*WByE=+rU2W+>tilVw2HxVgdy=&vaHxxFbe z?k_J;boN%Nrb!^I_$nYC(jTK;U^HPfG(h9%pYwp=MbJIYrKB|& zNQEi#wp!ctBLhyO7dg16&oDbd75rKBrx})N%-Z*7CC;nTPd1JownSicJ{aNqk~}tt z{&xyHtA7_AT&N57+t)X!SF4|d#69gF8(FueL1Z@YwPaPz7Urf-ENHA@Bpdw_udL=6 zu~JH)QokUFErTU#VxgqSIa9kahF)GVBEQMc#xbZ^o^(xV%UZL)Tq8tktf=jIlbBS^ zlB}W+sz>V4hzcN%#xJ|bSL#c+=CjQb#}i!S86e@iZ+dbYbpEpnXR17^qD-f?&3!pf zCM@p566mAtT>^5w$+nUqxY>WBpK&tGc>NJo#WPk8)8$}F1)J^cj6*xDVjtJ69{pcu zwg>_kKo1Roec`V%EafmsYG0Plz95=<>Q@O?xL^AVlPzZ9Z1zXGe1hcSG6qPpn2HIB z3e%mo>N)3F+8nVIq2#Xfb#5H&U+w}Egd=`s245tkB)`9PAwrOSij|G84C!t2*`Rq3 zqDH5j8hIMg1KyX0*;h4I#`ce^x$ROXp$n^$F14XqyJJQ#V3uo_G`37(82NUtubV21 z*{_1dPKgA)y)LeoDj#V4=nbE}+BCVBg^`mmlv4>CzgY=PynJ7sED=qj>{0gZX zW?5iA&BzAtM&cqRESGuYLf{ljCXv-|B5Xb1W*$^H5^X;19a!a`^n6-gyJw(yYa~Y+ zChQ8(Al)%H(93h>|Kuul3rK92%U}-O@B@P3pVl~r)p6kE*2hxPy|3RfwpnrC;{RpE z+kt}IrRDuFe}8@;MrqZ9HZGcyDl4FM&2(jRbAiWJq%RL#UU*ca=dq7UOuNO`e^2x& zqiIjc{?g~9Rnru(m&T-bwts@EjM1}%HxXO|uhW~aj(F@jC%b?$aH!fZ1j3Jg(`ztV zF`nFF_*bY^*`=lLwa*EQo z_*NPez1q^=mmu*q7)!a6K4N9T&kH03E0*FarG1l1PTtuxq@(B4oQ22SzNl$~XvIEd|zlnkV09{hfD z!y?E$!qRu~v<7{YUzZOLy&A@7%#cr5q_+k4Cu8SWTz>p1cc)O^u*7BgEkR9ev3cqX z{t|Kd+xw>Jb~+}SD{;!qPPsq3Eu5QUu1G0vm=XyoT~p5dPGNez2gvvwNC=@C+!4g9>Dw24OY~LPY-_Y>guicQI*|5$I{25k88>T^hy#qX3u+Bji;14%(NvJ2!NJ)-H}`TZ z&vkO)Qj|c1ZV@Da;9fwGba}qujpI#adW1iE9_G_GZ%PxACW8GYvoz$H_qz)iKHW&l zeQTcyQG7({mUtvWbChpnv}>j`n(y8pRK*I<;UqiYOuazk^9QTsnUJ#UPnK}oq2`O( zb4~U*jQwr}8G zi2JP6Zs}y5wzFwqZ8~XM-*m7UBZ}xY_|Kl`AAR8IE&BC@@_?VC{qF$V4gvBPu$yNC z`1rcjD&)ADaU7@Lvb{{7h!hVJq~rl@=g48H_YYt|jR1fF|7iNg(P@0}Le}9dKq94V zwzpKXM!j|5Q|9 z4T^Cth{_;7&6acGDpUOS&FH^SkyLL_$v4j4zwJ5(${+U`Vf)KH0)7HUwST+W1$gNn z@CHD{@vO`L1%%XSK*;}Z``=^FF1S~381DjiY=e_4jeT-nrcCvv1LX&*;?i+7I}1}! z?~fiGn4Zk6l|W_-z|q-2!(g|?PCj`G4Nw1h^CYFBO=Cx))~DN&+|oNU7Tl+{yS0qr zc)et5rp2v+JXun))uXR3Y&7`>Jj=MY@WjMV{*J5$v677;Fbhd?WVn!#2xmhZowi3` zP%xji4^i3jqQRcjQbg`Xt#r5XP9K#38CPMZ|HX)RfZK*7n&1iT9UnKf{vF4)5J#wH z&IY8Ckj)-j4R+s|>>u+tk@$}UR+yV>5zLDVFTam^X|fnmm7AFVrLU!FcS%Rr z3z~_RLUc7?zfsSgf-`X!Dhzx4VHwtUGvbzv=peiDF91OG%UMM7b84MfPqS@RO+CRk zA4%_BLY}i`D&C)2#JRzHX-5gV<4x-?zF^>36Y~C8dpwoI{~DF2EZJYgh0Rj9nNWaI z(cxz1$<7GI_r7ES@s{6$1w{M-VV22l<+DNqqt>}LD=!2G@L|vn1=YxojY;(3Y0mcg zJrR3VjKE`fIq56r#u2-2rwP-2LZu#Z4c!7fdtGU^n z*+FnShVr~a3PGkj=05QqNJ84ye!#O04#FVxCxh;%o| zH9mIpip$$cA9O0zgvL>jw6C41RggzENBUxfxTSyJlh)3Kna`3f*K-Y6u2u%5)*p5Y zB?@0sj}kTxp2GKiAMrO(gg+{~G&ou4IlpPzc!+!40($mpq^*Vqai!2`00aTC=SyjO znRT|)9MRbQzG9OIAo#GCdDP9T#8v+kSjqw2hHfo%W#k`-akDFe<|>zQ_tRT#^F51A zFt;i(7XU@njzJC;YJYZQ3iC0Xy`69A!6`p<+gH2RY;ouk8tjLI4g{o&;aUvN`*=te zcauUUBvNau^H+wLWRn!a6rYM={39dvJ|xj(*CBEAIY{%H7bPI{I2nBrDuLsB`p8A6 zSgmkj>DF3onFr37MQJbw@jlmI3oST#rSv$h^hIz`ZztQ@(QCi@Qg1pDpF_7>H9x11 z3whd6%NA*+%b09i?OiS8vaX20GeX)_=fm{xPeeZYS;Y9W{x-!Licv=86Pyi}KFaNZ z1!PZ;L+K7!C%LxV)JFdc(UXk82T2U4oB$@7% z894Gr;0%yYY(G5VJ5$v?9j24FF~tQ}#l?CAjPH@_RTCvv2WJmFZA6df zqsq6rhqT`rQaNS3_qI(R3{JEz&&`FpeY8Z<3LNDBKKDEved)V;y$qIcDiZ>^aP+-BP84Oh)Y>gsF5tz_Eh__dardn$@Hx{QDD%R_}B^i_utjf3`i;1O&F~pV0vnEB&A4Tpl4Xv z{)X6lHTrJre#0{%P^Mq_dV^`(Ci&O*W+O5LyO7TL|3M|E^T{6kR?4_#Hi>uU58)T_ zAFGcItP{IyeCyu0q!Z%h$?8hkzm^SVUD=)K*(*AuhGBoX2@S4!xhV=6?>qyl4V+h1A|u9g5h`%Pbg9`$Rlyy;=hwE5EX{;6E_?c1GHM>5%C$%xoLN&P!as6_ zY|1Wj3mcupQLX#!A z39q}Zf137U9=W}higH3gG!0)MnZsE0nL^aMNu)r=se$@Z`Do8*g2G2`u${F;-4&47 z5%YKu3DyrpRdJG&`es|r$bJr%=nSV675yb#Scj!|=+pUKS@iliT!JQqwdp`4oj;7_ z!kPI8wq9p|mOa){o@F@hDd)MFzi^v*cvYT*2`JTVh&0G!P3PHn;f7VQA(}BtN;_~Z-KMrnGto$8L!%`5#(I0a8YuQA( zVkCJs;gn8kPv&OWTz1u?)HV6pMPq{>IKATa)+?&=r#PeJr;QVmC)OW2;YE za?h5eXSuXueD@nKE0N^zf5HI4`lFf3JWJUY3vD(=$c)MC18=Kl-PINgdeEy9O;;?} zB5D?vniV*}0T7!Z=A5>AVvP1sSKkagE|pn*^Raqg>vbtv7zP<{BBnu^OR~B0;iQ@~ zQpqZr)t>x8ON0~olKCA9S^H_tiBy_l|6)((lKinaim4CFd+60>V}S zl1PDY>isdyz(RlTftjZA7Y6S9p9E~U*OSb|mQ?vj)jEA)XBeA= z$KtTiA90=~`8AQ2pG~3D<&!@luj6RyCQ7bJS6NIzt>Pj2I{6{>`8gO{(7lG^Xfg$KJh zF{V{J2dBWB^oh&wqcQesN3;BP-hmw5sO`?e+@Z;zC>uNVX&TeB?^JVz4T%B~Jw&aN z4P$ zal3FyLqr%oVFmM&-O$T33aLe;CvGdInThLB^)}?whDGE*j-0AxVr8UgZJp8P7X7zu zBfb*qyJvQuXjvH716kd^AEGbfjNXfK9CTb1T=5M^lLK9!)Hj&Gb%GgIN!gP&(IBBN zyFv#vf!+9=6!TP%Cgb`#zOb?y2m~rFo7#f_GPr*C?PH1zJDQw-lK9So5SlrSH{3D* zP5Q>go&OAseE;r%?LSE_=o}t{sjYFSrh`hM?)m?jNW%jV-oFh6(5sv308kWx4ckA6 z6#q>j00I=0|LZINzuGf>9_BukhQM@BiQ9w7!0$@dIgQ;t3CLMhv>JL~g+_tj23Swf zlq^|D5EZlMh@HR~ z9I`rf5eU)>7sn;WdM=s`ZkY7Ra7yH^`7Cw8wgYvXT0cXTKEDlJbMzZIIC_2EaLMPH ze8WVwaekKV^kBty{IvgXIukqS?`cwRFP4mt`o8t|oc4_9ckfK$bHp3F5Y2wM2mA&ChkV`O+Er|_Tbn2D#{2XZopHB} zmXB!~z7Q=slGJIm;PYJWbKDbD22_AvY_3D&KCzxm8O{}tmQp@LA;0P`Hrrnnx?FyK zEjR5oIK+F)yC;25G?}2ERxcBdG5ea8yYWY2*qh`2rU){^r$5+S+7>S-)6lf5QxS!3 z5w(9g3mI|0`k_Ds+t9}Q%91>|jPGbM{3?<31)KddaO$>1KpMz0%_W6(&+FhB80*lD z#|tO**l;JC+j+0wk%x#ih1b29maeT-9$uQk5-=>C%T7xj{S!UaU?AADDE8XsHX@L*x`P1=#eln_pcD~tR zA$~ur?fA=1)4|5(xlM8!mcJ`+r64_`C;a|+Jzm#{WaxN7{soHV{w5-a|Ag@ypqYJ6 z9`)YNp*kE&{f`L9SpEpm^3xJ-GE7I^(2c}z-3uH^HNSUwOfEC$hQ{a3saIYe(uz>} z^iyK#PPlAdd>!kEGE0$O&hghW8iu}J(606y6V#5>!K#QZaoEtPLU&+b+kAc^Bw-7% zQmz*ZleklD!lB3l48bz#d+r%AhB#sC^Y@JwX@^)*nv+D;Rw9JWH_%VF=97zmnAj-` z<|?WLT+cHIeGB4`snGLD#mUF~SeBNW&XtIbQW6p#S7cZ|Us@pg-U^O+CuNoK5R6zA z%<{b+8|D${VughoeYK!$ZLpJ+P3)gh*XxX@mNQsa_~{u5<^rb#d_B%y{AqkN*A#~} zXbehh$NZi~?u9RKbjq|LdaSWQ{K#@SR+y91Z<=u96)LT~-bz|A(_4Q5p?Hb1 z_=i>lZ}9=arTo1FoTF%3maFdDFOOyu6?fu!y#hd6?M@XjAC2*>&J3k;kbc=T<6zDyl18>CVkvBEY7*hSoz2LlBj ztez69`A^*gu5gE+sTM@CoZC^J?1<8qoXwkBETnzv6bGG{m0fu;x1qcW-t(O9Jr}kf zHP8ZP?2%VMgtm(>lV=qnYQNL*E3fVQ$78ufQch7Vdy=E3f@>Yr6f$ytYQ?C4#iI}g z)Fz(q5;$PG=N#XEER*Z@8c^(yq)9T=o`mF!Lv2X$U>+ZI2f5>?NAU(z@!}~yclpIF z8l1!!tbb>?TGd`O{&_JeLvhN3D_`H~l4{tkO{gjGa*ifCBA;-0&=oX#-Pk$tQJ%ut z6-7GLv@;juE^zZWO%zL2PHd?`#oc=2XC6VRzUHGlzKv3c1rDtM_k_3w*_>ujKf^wy+;^VdR&B-KW)AhrQP!gqaj@sAl+!#mU>(aRkR- z1!zR1r}%P&oPFaDeQXG+x{?Ktfu+F+X;L>PW#`*SvCjOhMCEK@hskE=<5OwB;$V1q8%pa5zZHIdI35X~I@F}9Vk;4134j(u zVCY@5(4fa@du!<@0(bid-M^-UD$eYl5jU;rPxBlry*QaTU??Us*_Ay|S=6lKEL zH_&%u#_@*XGJ0HG=-y7HqShwKi~adukVKub5^9(GO=l%nCS1k?3QLBPHJAeM6lLi< zP~|g4tyOQ7xug+(Zi5{!ROp^Qd;5yhlpYg8h^#Z)5PxKCz41QyiC6=x@0Kkt>dLQt z1t2$C%?++l0_BE1fnjzna75W+I(|8Q52BY#YK`7hNF-3PioEL(zb)@)$*l+2kWfrX z%~~`VS3d*O|z6w3mONHs|R z6qpdX#T|hKc-vB4KW$?r?&_Q{r_e-Wcs$facmjRZ$@=~!{rPzZBo{Jhi$8E~?uqrv zXq&(OLJC!Du~2{e4;EPZZwMPf3J59JgUqARhi4C-Gva9#3q`xvB~-%FN7-$$b|)yB ze%hdgQHtRdpJZFg!o;Y4Cqcf9QAZxFqT}Lj%kMDz5Z$IX=`sL9n1=w4R-%&|p#iNy zEJdL~-;xh~M8m;hRQDBotlo_{L|mAjev=fI@;gIX0$1tWFYmfq$dEi;V4>;azl4Q^ zT27s6(Y=}@3KAI}j)UA1KhJv~VeWM-iMwIn{Q0w#)SsPlL~nL_*>gfZM=oq9IsfxR zgtcQQQJCZQXp4;n|8HUC{Ve71^(}c=<7(j6*ChDV+H8M;pivGiSbqz5B~*LrRqCEq zj)^*nYoF!6K;7#5Kb~fPpjjn3!vf~Gp$x2gKw95YpD1^CKUf=c=_|4`*~$`Cx~A)l z@+1v*2`B*{s7~8^AX_e)zqOA?`Sg6MNRSijJN^HhE7%$?lsh5tyJ?%SPi_tuHhM&5 zZ(&^2z1(HHw4m^fRLcFIfaDKT{!u9(mY&iUGK3xwoms+5m%m}$9K zT3>AqQcno@!yRF4Mt5)UW*qYTogoX`yILLf#ph9u=K{*w+KF{Npc}Fz`zH*U)6R;* zYHjP0gA84+PgbD(m{TqecGlG}2SI&8*IGv%uO;VRuV8#*6Q$S> z6=w5AIqHTdfC1NfCBtoLj$MIC z-c9xIaj?dx)q#*i5?P$$1GOt7YTGQl1n3r#kfZhi@z}p7<{D({S^_C#lQ7^ zYs#hjtNOcxJR+0m?xvH=&)n3xI_K}MJscxym6Vm`?gRCn?eg~ySRb2GPJ|#2`o$-W z69YgJ5U(Qj^z25*#wu>yHc0>r=yoiS5}wyeT+!tjdm{IvM zpbp8)JA&j*TFt{?uPb9ltrgbaDXmXb>dMknC|A2|sTCEcYY2N%yukg94tQ!H0Y#WH zH2LWJ1HuGA+epmYg28R`dKnSpC_v$6%OQUaGnt@U*d4L)<$UuiaLin$2RBOq@F$PU zWioU-HwTeS{V3V}cy4a)@YEvP29SN)r4V^u#71kdsA$J{-rvz`Ql5_6?92w#)sX_b z>uH|CrbV@;fR?pOwz!~A&Qy&!lw`{a!Pjwr%o0-ulG3jk>&~Gco>B5h*FAHL4_+sj zENUYs6SA4ma2o}BBO*!5)bx#VzLH=!;zt2VNu*f6SjyPHGH z>G&vT2lltiE((8tlrXPl&29x0f=cYw)TzJD_o z)s-PxSAMIxXf(2aD2}ao+E{fGF(XZMV{POaO_kD%uT#RC=9F$lC~U=j{2zY zSstF>+!rqeiLia|H!@y)z(4fA^5;oaGbkbel(L9fGQ!Vrc9NlFdbb8yg+0ES%m$3M zVX2+Qq7wX{LR5*iNJe$q1inHjSRLTM<6STNt=PeZ*HwGPkQT z|I_rGWwzT@11#)(Dcy3*I6Y~3W z|D|pRQLQjvc%+KZM*dYeUUZ%BVpE-OFkh&;VB1W^w7}EDDGNyUT}->g>p8yEhah!q zA7NU~J@5iB%Z$Kbho;Ow>%u_x>FL;CL=A2w-u@ObdB_vcN zC)^nRGtM!x)th~H*6};L@qls#(36ymRfB`VYNZYHR`9OjzMk5sytjX=MjbQu_5p$M zKEpPKXHDQow(sySQO-A|3f^sKCp`fQuczCJzELVNXeIK*-^@C!L8p)}6(zEeqX3(? zXje+oHn48Y;@nPz17&s^)gx68934*hi^#hvV%o;{4VYt7)CG z;2KPj;CcU}2~SjGgd_DS=E}mp{-ubX{7I@cCSrunmd8!M5dg2fizI3RW+FDga?>EGJyjEM|oB^uce<(2nY-2 z$@pwyqd|57F4W&6iYppEP(go0DmL4{vA0pk4@wi97t9~i)ME4|pQcQ_8X_~<2kKIv ztSl&qjP~;NITAOgZrPhp4zt$tQF}F^(B;^eJZLSc+}3KY6QpXXxYSpp0go(nPi^kq z-24d6U#p+xA_7Of@|X#;vSNBIS(D)vIiy4`%b@Y?55fF^A!BOjVaYe3il5U`sbOX1 zg+KuXhvoL;@4%WGH=>f^)1MFi#I{U~Og{ZEf=!N{n=LbjcvT!TY-~swH3yz5*T4 z(h&kH;LjkHL1e;$6m^@Lw&?Sk?k2We?cD{@f-)|A-VfOA0`&`{55 zMWAg+wo)fNs5d7tDWh8*S9=%xn%WNU==8Lp${69xw8d=oG&MC9U!*itI=>l7KtKQx zPDRJWC{=*oy=${l1$rCTe}}cM()hyEErsL9BG)`@yt&4{HwJ4C`m!|7LnK^H8%#fxW=w5T%6%TZ2?k2pC^w^xcvqr(ZCNa_#xX5 zv~5Dx-T8Qw90#XW|1+~9Z2i?wYc9N+3aYH4Q~zB7&WgF+xdyHyk3Wqz#w2G`f0FC! z&JIs5;-U8tNK!6*WIB?~%9Rw_S77nz_MoW7ax{V6nAdsR_@lS_XJuvO93kPw;kMbg zEa_7Gz(@lg1}6w};iJ3KZ-w}i_>1E!13~6L&1d3{oDP5|#HfG2W*15}IIz4s4gLY} zaAbAB!t7oaL^+ACm+2*>~Co05_z_zePd_M9h8$ z#1Zd$%_{8%1wSMUX{xeDU#S`?f0Ae%c5|!e)&8o%x>GrBmZOx9R0g(V44dSPf;hlO zvTgM>vw*b_G174L|3FSOi6^Uy01fMJakXr(&*5c=K=HN15kHjKC0gZS`3bN}BXKuB z)7Yv`u`>?W_*WT-sQCuwhR@q8j&_<@ z4Y7WbBE>sjKA?3U(6i_{F%7episE?sIxoIVR38s5li_Lr;`oWh)Qe}p$_dmcnG*z) z8X8WrVaIWZ8XB&vGEG*az`t=6q-(^s^hin;%%f|fi?P4uN?dNj^v$FO_txt6!X=cc zoUa~F^*Nlaup}ALvgn!CLNmq08xtjOrGtOp5&XRY?g1b~?h^iN!5piu0Ytm|$L0pi zji#Ih>2515iz!}mtWCz3SpY>AoGre{(1;A8WX z2PicJL}_=S$Q4EO4I6tJeRIM_2-*kWj({4<)OZ%qE5K`^S7K)-Dn4_spwFbIxjJp* zG)XcW75kN`qC6SREW0XklBm>;No=@fdt|&}wgV}`u>$G8VVqo0K9R|2^?A$QbTgvu z%Z#62z?xz+58H|BO)S!p#ia7Xs zj41imu4v(;(q9$+5@Jb)Q|YrT%4fY)9<4^3!oF(mG+jUQnI%4_;jWxLB3aKo=>X)nt@9%#_f>$Sh8^RZ^4T|Upx;%ZfHE^i zyVFBnAuYQfrBMfM2t*SaQ})A*`7jlggiy7bs4;SA z48O@_l5w5#`H$-R@PW&S?xrRO)P)BA3x&1uz<_5lNjxtjYfZU7y`v@;_IQq)abK~| z7SMuojZ!!LGdWl#T(^FLhA6GBjYxopteAZ(+rxYzJOW*dDk3CkM07_vXt3&Pc8T-Sg0RCM+4+>%8KQa-x8 zz<=rr$kwZdFp9jqxwFW!^>nzQ?;2AH{;Qua5^8-L;T zCH;yRJ~gq8kHIKrvp1k-Y-(sPn14)c@r^T1a#f4L%7Ezv5>Kj;@yv&4u}QFMjJ7kB ztiQ&Q8@!gO{}KuXAl#viBfRyoW;hV&+tFy$=m22?x#LA`Fb;s4JKDB8=e8D4`X>ZD zdU%V6+%R7=GQ#tcPu$8m!-2;F27HdG|FOT{$_0CeWZf*Ap@Yp@#aK&y!hiRNs519{ zTR|K(D`Z<;u;IvT7cdpFX(*L$QDG0fi6YEesaInaj`KK>&343mTWr416{1wyVj(4A z;PBZow~0SsLg~CD>2VuDIlKk8Z(sXNR_Dy_980AQ^46#FfUb1Q$yMwfk#zefm{WXZ z@c=#4mPcPqNV1MU$wp_~WMl7%ejgwP^8(a=N)uP6G*K(u;MxnP$W$wo_vhEBD?ond zH9uv2pV&FKq>N@`TCRKuyumB1%I>6&y!bQZ4Kp)PU>hnHS}=KssP(rJ2TE}@YLy}NZUX<(JdjhacBM&btN=mXulR1$ ziDiyssFmH+5MS|KfehW)GNQlh=JLQ1H|;qipV}X!U5zX{Jz3(Cmx0b}j>&FZsBev|>KdYRH8k+j zut0AcVXlZ~Ubo*qcS0`xC0Vei%0VWvvfuW0f_`q)5=q~`HdWa431EvA0%nT<=#`%2 z2SR`#m6!*rdTZ2~oSdA-wrtG**?}T}B(N!FG)0tJ^zy*s0T69%9mEy{w)Y7yS#4IbgP$aR=R3NwaE*#WBX9U?0-Jn7Sq@bA0H zq3xu8#wx-;cz`%WGP}-?5!PPTJnqxL_}fl9_Y5%BS&15Dq~I3ft+wkbx22SWMdAG^ z?wyvX->H#>)xMhk?0CdV>Zby0e}Tv-KO2Bk0R~oA1t|*s5EUDtPTkop%urbJz@xS! zdIkm_N(R8er)YI)<&0#Y{3GD(#b80tZ_a+SF}aj^!eP5#x8fE$`!yLxrv-lt|JtSx zG)~t$c|_U5Po3-Y3*$Y%peK?p%Ajip)~X}GQN_358}z3VV;&2Oc^1ts>Zz{9gT@4( z$gY6jILi+nA7}nLzA8#`u|z0(9oqB`cdIm}TBRLP;UvQg%~7E;uJTwOalkrGWgtJ6vDU7%(sE?;PhuL+FN*LeDi~L1aZzcSXjq^Bj>nunCyIEN>Jhx4zw}wWp(Ij9T5GAAhn-3CB`usd##9 zs3ZJ;RCk_HO>J!g7Ew^7s3;)yTCS)dp(s^~6j6~PNDqV}AYBL$ItoY;0YQ3bDgqjc zG$B-}K}d-7PN)gJgOUqTTnDf;aJ>|Ed?*rRW5`Fg7F+&-$NNnDCAKBV30S#r2jT5tNQ_ulEH z+14n2xVqbQj8O6!AX8ehwb2T1O}0`M@JpS+hW9^2mGafw19j89o=&?hXN^qS{QMKE z-B}pv=O2EBy}ZVE=#JU~p|s^^U}rvXjeY2RM3}+L!PRHLi0GviY3&aa&qNk<91>I4 zP=nv!j^MTV<{Xaac}9JxKL^*h@5XpoQr(aByWn&>XPV1R;O@cehOc3tOJ%*YfncNoYMyfBkp=2Zs06 zBK2MAafS4~dL+B&3lV~K6=fSsoY#F%9uL@i1ZN;L72{RVJ>=8VkDte0?w_&CX7gN7 zH_?z8L`Zslgu4|EyE1tAq*!T)n_JNbY{&L;NO=bL8hJCg+z!MDJlFB#B$-sF8d7@E zgVOS4aAlf^)h}BH0LM~DHk(5QGgP_-$ZKFr^RE|$U%l3E6-XVO)l63KAxQ+UWHMVi zJe9hCzPyJ zdG(w-qN7D{O|lXO(My}o-u>f4PhRTYKx&pqi0%`jHi}Gd)%93eFTyy%cSV%=tMl7f z8LX6C<`%>e6bDXb^FqV_q!92r_tg(1&K2nK104g1p?~9-Ypf95u;o zO8#$t_us`C{#lBgZohUXQ*oPpwnP7H(Yvchx?jIW&!mqh<=?LE{5m{--s|*Y~V@J+IlT+S4F} z0uTK;eeXr#L;W@D8S{g}yBs@kZ#cIH-3oDUdb|I2EQdv^QaVL((ybe0HJpJRm);f!_=r#R zLY&2S3){=~gC{xY@=HH(O1xK(!a)U&Zllh-HVOCp`I1>_O0!;&Bz5Mi?VeS8f-@2s z`X{)Je7fOCx^;d+nVB;Cd~bi}g}6!!fuG9dd7EzE@2gpTGlzeuD7-6rLs9xU`}LQ% zQes7nj~q?rW|wwmp_hkp&?fpTXDUD8Min!pEn=>3ST@z3EQ~SQvw#iTb>1kNa-^H?G43J>H|DI z-%GJj+fn-05r4j(bM21yDLoPp58; zeR5%`tV;Zx>nY@_C~hqGUoTn_>t?fi_g#)p-wVmuUJ{&q-C~rYwjFXCM=NMy!%1Cl zH?N=8@YblivF`~ZvDkDu1Mv_nd6!3BPRM=)ZhsOfxXZHq)Q zM$+}-R|}&{vU6OS%E9mBqZ{U9yvfwy{tUjLx(uQ>0w7Nds9zYyv_<0?k$imG=m%)o zF+>tamyWS5HwLjrxQ?uo9+^eH$hj2ExOqP8N)!n3`r!QcC49*3=}A9P!d<&H#MLxfJLaD9h<9Cz&sfIToyy&FY+@oj-YMQFZK8y??giKR2L#sL zj?*006&oDvWiFP>TlWKl-K91N5c+14yXbdEW{!ya7*dwtY`dTVVK|k$p7!_YW|d*Z zfkYD!9Z|$oQ~@J18}(pqZB1sBmG7y)uZnCcT8s3fG@GHTZ1_dI)P=bIy-599sJm}ZdT-~|jl(6C3f_BQZj~;8 zveYapl;7|zc`Km><8@ejLEnIG`a~EbcMTK#iiMBp! z13HS`O@rn*cw{1^dq=a{kA^lo8KQH3gvglM!#Lk!M#O5u-2WN-mr%NuU^b(BiF4ap_Nz*7 z0qyeKP@>n6NdI?dW?H&qbmH%K&cO~%xHbQtWqt1MrPD8m-G4V@Uv}suy%Nff_bNzP z*>MvQVLtx|nnZQ3y^@vi!C-j2TXs zr#eN^Gm16k!0L+mmCC*%8-XK!K_4mJQAcnx9yonMnv$!y>HVn*og&5Cqy&$E zTR&Dbn|)4%4_oGCv;L{9>Anzpc&*FT+n&d&rv{rli|;AeJa=K09c(;Pj-BBhh58os z(wiweMMw{1Nb@zlh2lUcHx@k~+vV5alDS$>B`MT_uAU4JE*q?@4UZ;Y!pp7++d3~= zO+qm%s(x*e=P|fJNm4Ap!-lnJE1$8owSPMd`PwnjIc*vFNnu#q+GHqZmj^XIN zoheVxR4bD;w6bAVJvKV*mVvTyl@Uhw13WT=3vS1F?_PpPx0je@yS&Ip^ckOuSu}O- z^Esh}%{?_#k-5iv|1zYgRw&AA;V;5f2gvyTgB1%q;!yqnLK zaE-kodVx1M**=n=H1>YiUEG?MqiMs1@yjB&FZyg3PYy zjYgLP>*mb3&`eAxJ$(W@PYaZ`ZBQR#ZHLbUGHULoLG0;k+x4THOzl3;hFA8|_jh z{6h^NzmuZ6H?!`3A;!Cnn9}yC!`^%K>E*vt!@HoA^KqRbcw_i~z~;X^RvGF&p|atS z+LpfIVV58r5)t_XzLpMqZ8hSX7Ip4paX4EGt;nbJ$upwpI)R^Br};e!IP3=z=96B` z?t${6d;ROmVUJTIWn0hUBJc^$#pmVi=(EjE^^^j(B zkGgE;WWnKB;xRZMhW~ewF?U`4Vf%*&d*BtQDd_p2d_CgN!sRnPcBlW8S$Sl@j*rHzyo|piAIN2cP-Voau$!W=|vD*H=MZcKV`^jkys1Dlt z;Y*E1eClF+&6R=O1Kz|U4_>cchA#_Iksb%b(!}fWXBA(XOeI-qVJ5m=?%lBu5|eY4 z?&Tg1@em4$R@dq?rZcSBI_|qK<*7@SqyT_Jc9bDJcG^AqV`MFnDM-*(dd=(B?D(US zwlB*a3Xq~{=!KOVLBlUW^argjZ)k$U+z zi$h|AN}6#x0ZHDTK&s2FUb}v<-SWw~c_JY6TG%$^iKWuO

{S-6U&fL72n4Y1?U! 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 literal 0 HcmV?d00001 diff --git a/static/img/getting-started/v3-transaction-options-signing-account.png b/static/img/getting-started/v3-transaction-options-signing-account.png new file mode 100644 index 0000000000000000000000000000000000000000..dcb0388bd9369cb33a60877b0b73f081c8620f19 GIT binary patch literal 9680 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 literal 0 HcmV?d00001 diff --git a/static/img/getting-started/v3-transaction-submitted-modal.png b/static/img/getting-started/v3-transaction-submitted-modal.png new file mode 100644 index 0000000000000000000000000000000000000000..f0e77f1eaddab713488dfea468979c994c3be61e GIT binary patch literal 19415 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 k`iFn=H?3q= Date: Tue, 12 May 2026 20:27:12 +0300 Subject: [PATCH 06/67] Point v3 docs at platform.beta.enjin.io MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bulk URL update across 25 v3 docs pages — link/text content otherwise unchanged. v2 versioned docs and the unversioned src/pages/tools.md were intentionally left alone. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../01-getting-started/02-quick-start-guide.md | 2 +- .../02-creating-tokens/02-creating-tokens.md | 2 +- .../01-managing-tokens/06-create-qr-drops.md | 4 ++-- .../09-fetching-token-holders.md | 2 +- .../02-reading-user-wallets.md | 2 +- .../02-managing-users/04-using-fuel-tanks.md | 4 ++-- .../03-dynamic-metadata.md | 2 +- .../03-implementation-breakdown.md | 2 +- .../01-queries/01-transactions-queries.md | 2 +- .../01-queries/02-collections-queries.md | 2 +- .../01-queries/03-tokens-queries.md | 2 +- .../01-queries/04-wallets-queries.md | 2 +- .../01-queries/05-fuel-tank-queries.md | 2 +- .../01-queries/06-marketplace-queries.md | 2 +- .../01-queries/07-beam-queries.md | 18 +++++++++--------- .../02-mutations/01-transaction-mutations.md | 2 +- .../02-mutations/02-collections-mutations.md | 2 +- .../02-mutations/03-tokens-mutations.md | 2 +- .../02-mutations/04-wallets-mutations.md | 2 +- .../02-mutations/05-fuel-tanks-mutations.md | 2 +- .../02-mutations/06-marketplace-mutations.md | 2 +- .../02-mutations/07-beam-mutations.md | 2 +- docs/03-api-reference/03-api-reference.md | 2 +- .../04-enterprise-on-prem.md | 2 +- docs/05-enjin-platform/05-enjin-platform.md | 8 ++++---- 25 files changed, 38 insertions(+), 38 deletions(-) diff --git a/docs/01-getting-started/02-quick-start-guide.md b/docs/01-getting-started/02-quick-start-guide.md index 06dbab7..8ece84c 100644 --- a/docs/01-getting-started/02-quick-start-guide.md +++ b/docs/01-getting-started/02-quick-start-guide.md @@ -275,7 +275,7 @@ Congratulations! You've covered the basics of setting up your project on the Enj :::warning **Ready for Production?** Remember, this guide used the **Canary testnet** for development and testing. When you are ready to launch your application live, you will need to: -1. Set up your project on the **mainnet Enjin Platform Cloud**: https://platform.enjin.io/ +1. Set up your project on the **mainnet Enjin Platform Cloud**: https://platform.beta.enjin.io/ 2. Update your application's API calls or SDKs to target [**mainnet**](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md#selecting-a-network-and-chain) by passing `network: ENJIN` on each query and mutation. ::: diff --git a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md index 82edea4..eaec010 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md @@ -33,7 +33,7 @@ Before minting the Mainnet versions of your Tokens, that will be used in your li ## Option A. Using the Enjin Dashboard -In the Platform menu, navigate to "**[Tokens](https://platform.enjin.io/tokens)**". Then, click the "**[Create Token](https://platform.enjin.io/create/token)**" button. +In the Platform menu, navigate to "**[Tokens](https://platform.beta.enjin.io/tokens)**". Then, click the "**[Create Token](https://platform.beta.enjin.io/create/token)**" button. ![Creating Token](/img/guides/managing-tokens/creating-token.gif) diff --git a/docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md b/docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md index 306a541..3751ea2 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md +++ b/docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md @@ -78,11 +78,11 @@ Proceed to the [Freezing & Thawing](/02-guides/01-platform/01-managing-tokens/07 :::info Make sure to use the Beam Endpoint - Testnet: `http://platform.canary.enjin.io/graphql/beam` -- Mainnet: `http://platform.enjin.io/graphql/beam` +- Mainnet: `http://platform.beta.enjin.io/graphql/beam` **Try the Beam Playground here:** - Testnet: http://platform.canary.enjin.io/graphiql/beam -- Mainnet: http://platform.enjin.io/graphiql/beam +- Mainnet: http://platform.beta.enjin.io/graphiql/beam ::: First, create the Beam with the parameters of your choice, such as name, description, image, begin and end date & the number of assets that you would like to distribute. diff --git a/docs/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md b/docs/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md index 13a5607..22b55b8 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md +++ b/docs/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md @@ -661,7 +661,7 @@ For a comprehensive view of all available arguments for queries and mutations, p The `GetToken`, `GetTokens`, `GetCollection`, and `GetCollections` queries are limited only to collections and tokens that were created via the auth-ed Enjin Platform account. To use these queries with collections / tokens created externally (via a different Enjin Platform account / [NFT.io](https://nft.io) / [Enjin Console](https://console.enjin.io) / etc.) the collection must be "Tracked" first, or the query response will return an error. -To track a collection, head over to the collections page on Enjin Platform: https://platform.enjin.io/collections, click on the "Track" button, insert the collection ID of the collection you want to track and click "Track". +To track a collection, head over to the collections page on Enjin Platform: https://platform.beta.enjin.io/collections, click on the "Track" button, insert the collection ID of the collection you want to track and click "Track". ![Track Collection](/img/guides/managing-tokens/track-collection.png) diff --git a/docs/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md b/docs/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md index 687e483..993dc05 100644 --- a/docs/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md +++ b/docs/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md @@ -595,7 +595,7 @@ query FetchingWalletTokens{ ``` -curl -X POST "https://platform.enjin.io/graphql" \ +curl -X POST "https://platform.beta.enjin.io/graphql" \ -H "Content-Type: application/json" \ -H "Authorization: enjin_api_key" \ -d '{"query": "query FetchingWalletTokens { GetWallet(account: \"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f\") { tokenAccounts { edges { node { balance token { tokenId collection { collectionId } attributes { key value } } } } } } }"}' diff --git a/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md b/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md index c63eae4..5ecaa9c 100644 --- a/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md +++ b/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md @@ -105,10 +105,10 @@ Code snippets with these arguments are provided for illustrative purposes only. :::info Make sure to use the Fuel Tank Endpoint - Testnet: `http://platform.canary.enjin.io/graphql/fuel-tanks` -- Mainnet: `http://platform.enjin.io/graphql/fuel-tanks` +- Mainnet: `http://platform.beta.enjin.io/graphql/fuel-tanks` **Try the Beam Playground here:** - Testnet: http://platform.canary.enjin.io/graphiql/fuel-tanks -- Mainnet: http://platform.enjin.io/graphiql/fuel-tanks +- Mainnet: http://platform.beta.enjin.io/graphiql/fuel-tanks ::: ### Subsidize Token Transfers For A Collection diff --git a/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md b/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md index 0e7a7cb..edfe142 100644 --- a/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md +++ b/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md @@ -134,7 +134,7 @@ Finally, let's tell your collection where to find these new metadata files. You' 1. The `value` you need to set is your **``** from the last step, followed by the `/{id}.json` placeholder. * **Example Value:** `ipfs://bafy...xyz/{id}.json` (Replace `bafy...xyz` with your actual ``). -2. Set this attribute using the `BatchSetAttribute` mutation. You can copy/paste the example below into the [graphiql playground](https://platform.enjin.io/graphiql), and change the values. +2. Set this attribute using the `BatchSetAttribute` mutation. You can copy/paste the example below into the [graphiql playground](https://platform.beta.enjin.io/graphiql), and change the values. ```graphql mutation SetDynamicMetadataURI { diff --git a/docs/02-guides/01-platform/05-enjin-farmer-sample-game/03-implementation-breakdown.md b/docs/02-guides/01-platform/05-enjin-farmer-sample-game/03-implementation-breakdown.md index b59ebc0..8171c6d 100644 --- a/docs/02-guides/01-platform/05-enjin-farmer-sample-game/03-implementation-breakdown.md +++ b/docs/02-guides/01-platform/05-enjin-farmer-sample-game/03-implementation-breakdown.md @@ -36,7 +36,7 @@ The server is configured using the following environment variables: | :--- | :--- | | `PORT` | The port the server listens on. Defaults to `3000`. | | `JWT_SECRET` | A secure, random string used for signing player authentication tokens. | -| `ENJIN_API_URL` | The Enjin Platform API URL. Use `https://platform.canary.enjin.io/graphql` for testing (Canary Network) or `https://platform.enjin.io/graphql` for production. | +| `ENJIN_API_URL` | The Enjin Platform API URL. Use `https://platform.canary.enjin.io/graphql` for testing (Canary Network) or `https://platform.beta.enjin.io/graphql` for production. | | `ENJIN_API_KEY` | Your API Key Token obtained from the Enjin Platform. | | `DAEMON_WALLET_ADDRESS` | The address of your Wallet Daemon. This wallet receives the initial supply of all created tokens. | | `ENJIN_COLLECTION_ID` | The ID of the Enjin Farmer collection. If left blank, the server will create a new collection on startup. | diff --git a/docs/03-api-reference/01-queries/01-transactions-queries.md b/docs/03-api-reference/01-queries/01-transactions-queries.md index 1d274ad..1a9c314 100644 --- a/docs/03-api-reference/01-queries/01-transactions-queries.md +++ b/docs/03-api-reference/01-queries/01-transactions-queries.md @@ -15,7 +15,7 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere :::tip Core Endpoints - **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.enjin.io/graphql` +- **Mainnet:** `http://platform.beta.enjin.io/graphql` ::: This is an overview of some of the most commonly used operations in the Enjin Platform Schema. diff --git a/docs/03-api-reference/01-queries/02-collections-queries.md b/docs/03-api-reference/01-queries/02-collections-queries.md index eb5c740..3b16d08 100644 --- a/docs/03-api-reference/01-queries/02-collections-queries.md +++ b/docs/03-api-reference/01-queries/02-collections-queries.md @@ -14,7 +14,7 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere :::tip Core Endpoints - **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.enjin.io/graphql` +- **Mainnet:** `http://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/01-queries/03-tokens-queries.md b/docs/03-api-reference/01-queries/03-tokens-queries.md index 0a707ae..d061300 100644 --- a/docs/03-api-reference/01-queries/03-tokens-queries.md +++ b/docs/03-api-reference/01-queries/03-tokens-queries.md @@ -14,7 +14,7 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere :::tip Core Endpoints - **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.enjin.io/graphql` +- **Mainnet:** `http://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/01-queries/04-wallets-queries.md b/docs/03-api-reference/01-queries/04-wallets-queries.md index fd65501..132db70 100644 --- a/docs/03-api-reference/01-queries/04-wallets-queries.md +++ b/docs/03-api-reference/01-queries/04-wallets-queries.md @@ -14,7 +14,7 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere :::tip Core Endpoints - **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.enjin.io/graphql` +- **Mainnet:** `http://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/01-queries/05-fuel-tank-queries.md b/docs/03-api-reference/01-queries/05-fuel-tank-queries.md index 8a9e7d8..2555ad0 100644 --- a/docs/03-api-reference/01-queries/05-fuel-tank-queries.md +++ b/docs/03-api-reference/01-queries/05-fuel-tank-queries.md @@ -14,7 +14,7 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere :::tip Fuel Tanks Endpoints - **Testnet:** `http://platform.canary.enjin.io/graphql/fuel-tanks` -- **Mainnet:** `http://platform.enjin.io/graphql/fuel-tanks` +- **Mainnet:** `http://platform.beta.enjin.io/graphql/fuel-tanks` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/01-queries/06-marketplace-queries.md b/docs/03-api-reference/01-queries/06-marketplace-queries.md index f18084f..1b20481 100644 --- a/docs/03-api-reference/01-queries/06-marketplace-queries.md +++ b/docs/03-api-reference/01-queries/06-marketplace-queries.md @@ -14,7 +14,7 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere :::tip Marketplace Endpoints - **Testnet:** `http://platform.canary.enjin.io/graphql/marketplace` -- **Mainnet:** `http://platform.enjin.io/graphql/marketplace` +- **Mainnet:** `http://platform.beta.enjin.io/graphql/marketplace` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/01-queries/07-beam-queries.md b/docs/03-api-reference/01-queries/07-beam-queries.md index b03aa05..ba8702e 100644 --- a/docs/03-api-reference/01-queries/07-beam-queries.md +++ b/docs/03-api-reference/01-queries/07-beam-queries.md @@ -15,7 +15,7 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere :::tip Beam Endpoints - **Testnet:** `http://platform.canary.enjin.io/graphql/beam` -- **Mainnet:** `http://platform.enjin.io/graphql/beam` +- **Mainnet:** `http://platform.beta.enjin.io/graphql/beam` ::: This is a detailed reference guide that explains the most commonly used operations. @@ -74,8 +74,8 @@ query GetBeam { "isClaimable": true, "claimsRemaining": 10, "qr": { - "url": "https://platform.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc", - "payload": "https://platform.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc" + "url": "https://platform.beta.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc", + "payload": "https://platform.beta.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc" }, "message": null, "collection": null @@ -186,8 +186,8 @@ query GetBeams { "end": "2028-11-14T00:00:00+00:00", "isClaimable": true, "qr": { - "url": "https://platform.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc", - "payload": "https://platform.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc" + "url": "https://platform.beta.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc", + "payload": "https://platform.beta.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc" }, "collection": null, "claims": { @@ -582,8 +582,8 @@ query GetSingleUseCodes { "node": { "code": "eyJpdiI6IlNFM2tRZzgxaU5pZi9hVXBVZTBCeVE9PSIsInZhbHVlIjoicDdsSmlCU1JHNnd6QUtHU1FRV2pNQ2twaDNPT2YreFVrK3Jrb1VXL0NPc2tOeFBVdTZFM2J1Ykp5NTlRNlNwZXg0d1JTbHo1N1BHMDJqaG11WDhDTGIxRVlVenBoTWVwT3ZrL3RwR2pZTzA9IiwibWFjIjoiYWI3ZjdhY2RmMzE2NGM1MjAxMWVmYTk2ZWNjNWQyNTY4MWQ4NGZhY2ZhN2M5Y2FmMmM0MzU5MDM1N2Q1NDFiYSIsInRhZyI6IiJ9", "qr": { - "url": "https://platform.enjin.io/claim/eyJpdiI6ImVMNTEybWEwSnZ6WlF3QjUvQW15Wmc9PSIsInZhbHVlIjoiVklOcCtCYVpGTG1aRjQyY0FZb3RmS1JSd1JGT0dka05rT1NQUnZoMXZ4UGsxVklzVmNoejhEUDQ5TjVMc0picjR4MmEzQUJTaWMrUVYxb21yMExZRWVjUGp1T2Rnb0owMHcwWmxXWCtGMnM9IiwibWFjIjoiMjUyNzdlODkyMTRkZmViM2M1OWY5MWNlYjgxYjUzOGE3NTA4NmU0NjY3MjIwOGY0ZDkxNTg3ZTJjOWM5NjUxMSIsInRhZyI6IiJ9", - "payload": "https://platform.enjin.io/claim/eyJpdiI6IkJJWVNYb3lFVGRpaUNEemNJQ20xamc9PSIsInZhbHVlIjoic1ZXMzNGN1FnbWVRc1Z2S2IrYThIT0Rpd2JpeGhza2wxSG1FSlVqTUhzN3UrYmozaXNnZ2dSTU5HTmluZ2laY3JWNUcyemcyaDJZT1VGdGx6SjgxdGVwdUIvN1JhRHRVSnd2RFRYZVhOOUk9IiwibWFjIjoiNzllYmQ1MjFlODBmMWEzOTdjOTc0YWM0YjQ4M2ExZmEyOWZhNzQyY2RlNGJjMzcxNDdkOTQxOWJlYmYyMmUwMSIsInRhZyI6IiJ9" + "url": "https://platform.beta.enjin.io/claim/eyJpdiI6ImVMNTEybWEwSnZ6WlF3QjUvQW15Wmc9PSIsInZhbHVlIjoiVklOcCtCYVpGTG1aRjQyY0FZb3RmS1JSd1JGT0dka05rT1NQUnZoMXZ4UGsxVklzVmNoejhEUDQ5TjVMc0picjR4MmEzQUJTaWMrUVYxb21yMExZRWVjUGp1T2Rnb0owMHcwWmxXWCtGMnM9IiwibWFjIjoiMjUyNzdlODkyMTRkZmViM2M1OWY5MWNlYjgxYjUzOGE3NTA4NmU0NjY3MjIwOGY0ZDkxNTg3ZTJjOWM5NjUxMSIsInRhZyI6IiJ9", + "payload": "https://platform.beta.enjin.io/claim/eyJpdiI6IkJJWVNYb3lFVGRpaUNEemNJQ20xamc9PSIsInZhbHVlIjoic1ZXMzNGN1FnbWVRc1Z2S2IrYThIT0Rpd2JpeGhza2wxSG1FSlVqTUhzN3UrYmozaXNnZ2dSTU5HTmluZ2laY3JWNUcyemcyaDJZT1VGdGx6SjgxdGVwdUIvN1JhRHRVSnd2RFRYZVhOOUk9IiwibWFjIjoiNzllYmQ1MjFlODBmMWEzOTdjOTc0YWM0YjQ4M2ExZmEyOWZhNzQyY2RlNGJjMzcxNDdkOTQxOWJlYmYyMmUwMSIsInRhZyI6IiJ9" } } }, @@ -592,8 +592,8 @@ query GetSingleUseCodes { "node": { "code": "eyJpdiI6Imp5R0xzNC9aVWZlT090SGVsL3RyVlE9PSIsInZhbHVlIjoiemJiWmsrZEl2TXVzc2R1c2svalhIRjdGVUFXRThPOTg2RnZJVCtMNEE1UENxN3FLeURhL1J4UXhBakduekduRmxRQTJwdmRCeXZqQi9rbVVyVnRmK2V4c2xDZU9jUW15N0xNSkRiUyt2ZlE9IiwibWFjIjoiNTZlOGQ4NjdmNDNmOTQ3NWYwM2M2MzRlMGRmODlmNmQwZmM2ZDllNjdmMTk4ZTA0ZWJiYTliZTZkZTJhOTM3MyIsInRhZyI6IiJ9", "qr": { - "url": "https://platform.enjin.io/claim/eyJpdiI6IlhtTHdEdlM5bXlQNnVEaGZpejkrR3c9PSIsInZhbHVlIjoidlh3dVhuTVhOVEQvcElBM0drMVhBQ2Z1SFdob3BtNmJnYVQwWExoVmlUYWdPTUhkUzJ1QS9JL25yeHVPSnBQb2VyM3lJWXd3aEMySVd1MjkwellTdW50UVl1YTJRdmNCV3lzdXVLaE9Wckk9IiwibWFjIjoiOGNhNmY1MzZlZWRjMmE0ZjMwYjQ0ODY0Nzg5MTk5MDU4OTAwYmZhYjYzODI4OGQ3ZDIyNTQ5MjA2Zjc2MTczYiIsInRhZyI6IiJ9", - "payload": "https://platform.enjin.io/claim/eyJpdiI6IkN4OHZzUDdGc1AwUG9wdXpQSDVGYXc9PSIsInZhbHVlIjoiOTJJRHQ3VjdoZHR0WVpqajVoSGZIZFNoZHJtVE1iRjR0ZndLQXRQYVE1TS8zaGd1WEZDdDQrckNzUXExdnFvZDVVcE5PMVU1bklFRWhqSnVXVi96WVB4RE5CMk1qTFhHYVIvVWdscFljUTA9IiwibWFjIjoiYWIyY2U4YmIzMjJmNjY0ZmE2NzZjMTZhYTE0YjJlMTEwNDQxMjJlY2Y3NzZlYzZkMDNhMGI3NGQ2MDEyYTZlZSIsInRhZyI6IiJ9" + "url": "https://platform.beta.enjin.io/claim/eyJpdiI6IlhtTHdEdlM5bXlQNnVEaGZpejkrR3c9PSIsInZhbHVlIjoidlh3dVhuTVhOVEQvcElBM0drMVhBQ2Z1SFdob3BtNmJnYVQwWExoVmlUYWdPTUhkUzJ1QS9JL25yeHVPSnBQb2VyM3lJWXd3aEMySVd1MjkwellTdW50UVl1YTJRdmNCV3lzdXVLaE9Wckk9IiwibWFjIjoiOGNhNmY1MzZlZWRjMmE0ZjMwYjQ0ODY0Nzg5MTk5MDU4OTAwYmZhYjYzODI4OGQ3ZDIyNTQ5MjA2Zjc2MTczYiIsInRhZyI6IiJ9", + "payload": "https://platform.beta.enjin.io/claim/eyJpdiI6IkN4OHZzUDdGc1AwUG9wdXpQSDVGYXc9PSIsInZhbHVlIjoiOTJJRHQ3VjdoZHR0WVpqajVoSGZIZFNoZHJtVE1iRjR0ZndLQXRQYVE1TS8zaGd1WEZDdDQrckNzUXExdnFvZDVVcE5PMVU1bklFRWhqSnVXVi96WVB4RE5CMk1qTFhHYVIvVWdscFljUTA9IiwibWFjIjoiYWIyY2U4YmIzMjJmNjY0ZmE2NzZjMTZhYTE0YjJlMTEwNDQxMjJlY2Y3NzZlYzZkMDNhMGI3NGQ2MDEyYTZlZSIsInRhZyI6IiJ9" } } } diff --git a/docs/03-api-reference/02-mutations/01-transaction-mutations.md b/docs/03-api-reference/02-mutations/01-transaction-mutations.md index f1c3b72..caba563 100644 --- a/docs/03-api-reference/02-mutations/01-transaction-mutations.md +++ b/docs/03-api-reference/02-mutations/01-transaction-mutations.md @@ -14,7 +14,7 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere :::tip Core Endpoints - **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.enjin.io/graphql` +- **Mainnet:** `http://platform.beta.enjin.io/graphql` ::: This is an overview of some of the most commonly used operations in the Enjin Platform Schema. diff --git a/docs/03-api-reference/02-mutations/02-collections-mutations.md b/docs/03-api-reference/02-mutations/02-collections-mutations.md index bb51eae..c4e946a 100644 --- a/docs/03-api-reference/02-mutations/02-collections-mutations.md +++ b/docs/03-api-reference/02-mutations/02-collections-mutations.md @@ -14,7 +14,7 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere :::tip Core Endpoints - **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.enjin.io/graphql` +- **Mainnet:** `http://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/02-mutations/03-tokens-mutations.md b/docs/03-api-reference/02-mutations/03-tokens-mutations.md index f45639e..c077d0d 100644 --- a/docs/03-api-reference/02-mutations/03-tokens-mutations.md +++ b/docs/03-api-reference/02-mutations/03-tokens-mutations.md @@ -15,7 +15,7 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere :::tip Core Endpoints - **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.enjin.io/graphql` +- **Mainnet:** `http://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/02-mutations/04-wallets-mutations.md b/docs/03-api-reference/02-mutations/04-wallets-mutations.md index 7af0d0f..8c9e3c0 100644 --- a/docs/03-api-reference/02-mutations/04-wallets-mutations.md +++ b/docs/03-api-reference/02-mutations/04-wallets-mutations.md @@ -14,7 +14,7 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere :::tip Core Endpoints - **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.enjin.io/graphql` +- **Mainnet:** `http://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/02-mutations/05-fuel-tanks-mutations.md b/docs/03-api-reference/02-mutations/05-fuel-tanks-mutations.md index a5d9ef3..482f20b 100644 --- a/docs/03-api-reference/02-mutations/05-fuel-tanks-mutations.md +++ b/docs/03-api-reference/02-mutations/05-fuel-tanks-mutations.md @@ -14,7 +14,7 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere :::tip Fuel Tanks Endpoints - **Testnet:** `http://platform.canary.enjin.io/graphql/fuel-tanks` -- **Mainnet:** `http://platform.enjin.io/graphql/fuel-tanks` +- **Mainnet:** `http://platform.beta.enjin.io/graphql/fuel-tanks` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/02-mutations/06-marketplace-mutations.md b/docs/03-api-reference/02-mutations/06-marketplace-mutations.md index 7105d7b..b07e3c8 100644 --- a/docs/03-api-reference/02-mutations/06-marketplace-mutations.md +++ b/docs/03-api-reference/02-mutations/06-marketplace-mutations.md @@ -14,7 +14,7 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere :::tip Marketplace Endpoints - **Testnet:** `http://platform.canary.enjin.io/graphql/marketplace` -- **Mainnet:** `http://platform.enjin.io/graphql/marketplace` +- **Mainnet:** `http://platform.beta.enjin.io/graphql/marketplace` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/02-mutations/07-beam-mutations.md b/docs/03-api-reference/02-mutations/07-beam-mutations.md index 9b29227..36afbd9 100644 --- a/docs/03-api-reference/02-mutations/07-beam-mutations.md +++ b/docs/03-api-reference/02-mutations/07-beam-mutations.md @@ -14,7 +14,7 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere :::tip Beam Endpoints - **Testnet:** `http://platform.canary.enjin.io/graphql/beam` -- **Mainnet:** `http://platform.enjin.io/graphql/beam` +- **Mainnet:** `http://platform.beta.enjin.io/graphql/beam` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/03-api-reference.md b/docs/03-api-reference/03-api-reference.md index d2b9c57..0c5a327 100644 --- a/docs/03-api-reference/03-api-reference.md +++ b/docs/03-api-reference/03-api-reference.md @@ -13,7 +13,7 @@ The GraphiQL Playground is an interactive environment where developers can explo You can access the GraphiQL Playground for both Testnet and Mainnet environments: - **Testnet**: [Core Operations](https://platform.canary.enjin.io/graphiql) -- **Mainnet**: [Core Operations](https://platform.enjin.io/graphiql) +- **Mainnet**: [Core Operations](https://platform.beta.enjin.io/graphiql) ### Navigating the Documentation Explorer diff --git a/docs/05-enjin-platform/04-enterprise-on-prem/04-enterprise-on-prem.md b/docs/05-enjin-platform/04-enterprise-on-prem/04-enterprise-on-prem.md index aa3c93a..181fc62 100644 --- a/docs/05-enjin-platform/04-enterprise-on-prem/04-enterprise-on-prem.md +++ b/docs/05-enjin-platform/04-enterprise-on-prem/04-enterprise-on-prem.md @@ -5,7 +5,7 @@ description: "Find out how to set up enterprise on-prem for your blockchain infr --- :::info Choosing the right platform for you There are two version of the Enjin Platform -- The [Enjin Platform Cloud](http://platform.enjin.io/) +- The [Enjin Platform Cloud](http://platform.beta.enjin.io/) - A cloud-hosted version that you can start using straight away. - Ideal if you're new to Enjin or don't want to manage your own infrastructure. - The [ Enterprise On-Prem Enjin Platform](https://github.com/enjin/platform) diff --git a/docs/05-enjin-platform/05-enjin-platform.md b/docs/05-enjin-platform/05-enjin-platform.md index 9f88720..c723aa1 100644 --- a/docs/05-enjin-platform/05-enjin-platform.md +++ b/docs/05-enjin-platform/05-enjin-platform.md @@ -5,7 +5,7 @@ description: "Discover the Enjin Platform, a comprehensive suite of tools for bu --- :::info There are three versions of the Enjin Platform to choose from: - The [Enjin Platform Cloud (Testnet)](https://platform.canary.enjin.io/login), connected to Enjin Canary Testnet -- The [Enjin Platform Cloud (Mainnet)](http://platform.enjin.io/), connected to Enjin Blockchain Mainnet +- The [Enjin Platform Cloud (Mainnet)](http://platform.beta.enjin.io/), connected to Enjin Blockchain Mainnet - The [Enterprise On-Prem Enjin Platform](https://github.com/enjin/platform) where you can choose to connect to either network. ::: @@ -19,7 +19,7 @@ Consider the Enjin Platform as your digital interpreter, simplifying your commun The Enjin Platform works much like a customizable building set. It has a modular design, which means you can add or take away pieces as needed. These pieces come in the form of optional add-on packages. They provide extra tools, such as managing Fuel Tanks, handling marketplace listings (through the marketplace package), or generating Enjin Beams via the API (with the Enjin Beam package). In other words, with the Enjin Platform, you get a customized setup that fits your specific needs. -This versatile platform offers two versions to cater to different needs. The [Enterprise On-Prem](https://github.com/enjin/platform), self-hosted version provides you with comprehensive control over your data, eliminating dependence on any third-party servers or services. Enjin also offers a hosted version, the Enjin Platform Cloud ([mainnet ](https://platform.enjin.io/)\|[ canary](https://platform.canary.enjin.io/)), tailored for users seeking rapid integration. You can choose the version that best fits your requirements. +This versatile platform offers two versions to cater to different needs. The [Enterprise On-Prem](https://github.com/enjin/platform), self-hosted version provides you with comprehensive control over your data, eliminating dependence on any third-party servers or services. Enjin also offers a hosted version, the Enjin Platform Cloud ([mainnet ](https://platform.beta.enjin.io/)\|[ canary](https://platform.canary.enjin.io/)), tailored for users seeking rapid integration. You can choose the version that best fits your requirements. ![](/img/components/4.png) @@ -96,7 +96,7 @@ The Enjin Platform comes with a lightweight and easy-to-use user interface. The ![](/img/components/6.png) -The user interface is available as a [standalone application](https://github.com/enjin/platform-ui) for the Enterprise On-Prem Enjin Platform, but comes standard when using the Enjin-hosted implementation of the [Enjin Platform](https://platform.enjin.io/). +The user interface is available as a [standalone application](https://github.com/enjin/platform-ui) for the Enterprise On-Prem Enjin Platform, but comes standard when using the Enjin-hosted implementation of the [Enjin Platform](https://platform.beta.enjin.io/). ## Enjin Platform Cloud @@ -104,4 +104,4 @@ The modularity and flexibility of the Enjin Platform can be daunting for first-t This user-friendly version allows you to kickstart your next NFT project in just a few minutes. Ran on Enjin's robust servers, yet fully secure and isolated to your unique project instance, you can launch your NFT game with a few simple steps. Just create an account, set it up, generate your first API key, and voila! Your NFT game is operational. The [Wallet Daemon](https://github.com/enjin/wallet-daemon) on your own server securely handles the signing and broadcasting of all transactions, ensuring the utmost integrity of your project's security. -Get started today on [mainnet](https://platform.enjin.io/register) or [canary](https://platform.canary.enjin.io/register). +Get started today on [mainnet](https://platform.beta.enjin.io/register) or [canary](https://platform.canary.enjin.io/register). From 2a6c789aece60eb79c9433ec9ba77508a50b2105 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Tue, 12 May 2026 21:06:27 +0300 Subject: [PATCH 07/67] Collapse Testnet/Mainnet callouts to single endpoint; swap remaining canary URLs For v3 the platform is network-agnostic, so the API reference pages, the fuel-tanks guide, and the api-reference landing page no longer split endpoints by network. Replaced the Testnet/Mainnet tip callouts with a single "GraphQL Endpoint" callout pointing at platform.beta.enjin.io. Quick-start step 1 now points at the unified platform URL. Bulk-swapped the remaining canary URLs in code samples and link references. Intentionally left untouched (slated for full page migrations later): - docs/05-enjin-platform/05-enjin-platform.md (platform v3 closed source) - docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md (beams removed in v3) - farmer sample game ENJIN_API_URL rows (setup-guide:86, implementation-breakdown:39) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../02-quick-start-guide.md | 18 +++---- .../01-creating-collections.md | 14 ++--- .../010-transfer-accept-collection.md | 20 +++---- .../02-creating-a-currency-token.md | 10 ++-- .../02-creating-tokens/02-creating-tokens.md | 12 ++--- .../01-managing-tokens/03-adding-metadata.md | 14 ++--- .../01-managing-tokens/04-minting-a-token.md | 14 ++--- .../05-transferring-tokens.md | 34 ++++++------ .../01-managing-tokens/07-freezing-thawing.md | 50 +++++++++--------- .../08-burning-destroying-tokens.md | 34 ++++++------ .../09-fetching-token-holders.md | 24 ++++----- .../01-using-wallet-connect.md | 10 ++-- .../02-verifying-wallets.md | 38 +++++++------- .../02-reading-user-wallets.md | 34 ++++++------ .../03-using-managed-wallets.md | 48 ++++++++--------- .../02-managing-users/04-using-fuel-tanks.md | 52 +++++++++---------- .../04-brand-collab-nfts.md | 2 +- .../03-advanced-mechanics/05-enj-infusion.md | 10 ++-- .../01-setup-guide.md | 6 +-- .../01-queries/01-transactions-queries.md | 5 +- .../01-queries/02-collections-queries.md | 5 +- .../01-queries/03-tokens-queries.md | 5 +- .../01-queries/04-wallets-queries.md | 5 +- .../01-queries/05-fuel-tank-queries.md | 5 +- .../01-queries/06-marketplace-queries.md | 5 +- .../01-queries/07-beam-queries.md | 5 +- .../02-mutations/01-transaction-mutations.md | 5 +- .../02-mutations/02-collections-mutations.md | 5 +- .../02-mutations/03-tokens-mutations.md | 5 +- .../02-mutations/04-wallets-mutations.md | 5 +- .../02-mutations/05-fuel-tanks-mutations.md | 5 +- .../02-mutations/06-marketplace-mutations.md | 5 +- .../02-mutations/07-beam-mutations.md | 5 +- docs/03-api-reference/03-api-reference.md | 5 +- 34 files changed, 250 insertions(+), 269 deletions(-) diff --git a/docs/01-getting-started/02-quick-start-guide.md b/docs/01-getting-started/02-quick-start-guide.md index 8ece84c..cc4ad58 100644 --- a/docs/01-getting-started/02-quick-start-guide.md +++ b/docs/01-getting-started/02-quick-start-guide.md @@ -28,7 +28,7 @@ The Enjin Platform is your central hub for managing blockchain projects, collect ### Steps to Create an Account: -1. Navigate to the Canary Platform website: https://platform.canary.enjin.io/ +1. Navigate to the Enjin Platform: https://platform.beta.enjin.io/ 2. Click on the "Create an account" button. 3. Fill in your account credentials and click on the "Register" button. 4. Verify your email address by clicking the link sent to your inbox. @@ -81,21 +81,21 @@ You can interact with the Enjin Platform and initiate blockchain requests in sev ::: 1. **Create Collection:** - - In the Platform menu, navigate to the **[Collections page](https://platform.canary.enjin.io/collections)**. Then, click on the "**[Create Collection](https://platform.canary.enjin.io/create/collection)**" button. + - In the Platform menu, navigate to the **[Collections page](https://platform.beta.enjin.io/collections)**. Then, click on the "**[Create Collection](https://platform.beta.enjin.io/create/collection)**" button. ![Creating a collection](/img/guides/managing-tokens/create-collection.gif) - Customize your collection - you can specify the collection's name, description, media, attributes, Mint and Market policies, and more. To learn more about collection configuration, please check the [Create Collections guide](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md). - - Submit the creation request by clicking on the **Create** button. This will create a signature request which you can view in the [Transactions page](https://platform.canary.enjin.io/transactions), and will require a transaction to be signed. + - Submit the creation request by clicking on the **Create** button. This will create a signature request which you can view in the [Transactions page](https://platform.beta.enjin.io/transactions), and will require a transaction to be signed. ![](/img/guides/managing-tokens/pending-create-collection-txn.png) - To sign the Create Collection transaction request - Click on the **"Sign"** button and approve the signature request with your connected wallet. - - Once the transaction is confirmed on-chain, the new collection will appear in your [Collections page](https://platform.canary.enjin.io/collections), where you can also find its ID, keep note of it for the next step. + - Once the transaction is confirmed on-chain, the new collection will appear in your [Collections page](https://platform.beta.enjin.io/collections), where you can also find its ID, keep note of it for the next step. 2. **Create Token:** - - In the Platform menu, navigate to the **[Tokens page](https://platform.canary.enjin.io/tokens)**. Then, click on the "**[Create Token](https://platform.canary.enjin.io/create/token)**" button. + - In the Platform menu, navigate to the **[Tokens page](https://platform.beta.enjin.io/tokens)**. Then, click on the "**[Create Token](https://platform.beta.enjin.io/create/token)**" button. - Customize your token - you can specify the token's name, description, media, attributes, and more. To learn more about token configuration, please check the [Create Tokens guide](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md). - In the **"Collection ID"** input field, insert the collection id of the collection you just created. You may also click the input field and select the id from the dropdown menu. - - In the **"Recipient"** input field, insert the of the wallet that you wish to receive the token's initial supply. You can find the address of your connected wallet in the [settings page](https://platform.canary.enjin.io/settings) by clicking on the Settings icon in the top right corner, followed by the "Settings" button. - - Submit the creation request by clicking on the **Create** button. This requires another transaction, which you can sign in the [Transactions page](https://platform.canary.enjin.io/transactions). + - In the **"Recipient"** input field, insert the of the wallet that you wish to receive the token's initial supply. You can find the address of your connected wallet in the [settings page](https://platform.beta.enjin.io/settings) by clicking on the Settings icon in the top right corner, followed by the "Settings" button. + - Submit the creation request by clicking on the **Create** button. This requires another transaction, which you can sign in the [Transactions page](https://platform.beta.enjin.io/transactions). :::tip Programmatic Creation You can also create collections and tokens using the API or SDKs. @@ -135,7 +135,7 @@ In this example, we will be using the In a production-ready integration, your app will typically send API calls using HTTP requests or one of the [official Enjin SDKs](/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md). You can learn more about this in the [Using the Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) page. ::: -To interact with the Enjin Platform API, head over to the [Enjin Platform](https://platform.canary.enjin.io/), click on the settings icon in the top right corner, followed by **"platform-core"** in the available packages. This will navigate to the [Enjin Platform GraphiQL Playground](https://platform.canary.enjin.io/graphiql/). +To interact with the Enjin Platform API, head over to the [Enjin Platform](https://platform.beta.enjin.io/), click on the settings icon in the top right corner, followed by **"platform-core"** in the available packages. This will navigate to the [Enjin Platform GraphiQL Playground](https://platform.beta.enjin.io/graphiql/). Once you are in the GraphiQL playground page, copy & paste the following query: @@ -220,7 +220,7 @@ mutation TransferToken{ ``` :::note This request needs to be signed. -You can sign it in the [Transactions page](https://platform.canary.enjin.io/transactions), as we did in the previous steps. +You can sign it in the [Transactions page](https://platform.beta.enjin.io/transactions), as we did in the previous steps. ::: :::tip Learn more about sending tokens diff --git a/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md b/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md index b981831..b157c6a 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md +++ b/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md @@ -23,7 +23,7 @@ On Enjin Blockchain, every token must be placed inside a ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation CreateCollection($forceCollapsingSupply: Boolean) {\r\n CreateCollection(\r\n mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"forceCollapsingSupply":false}}' @@ -99,7 +99,7 @@ createCollection.Fragment(transactionFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -140,7 +140,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -182,7 +182,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -210,7 +210,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation CreateCollection($forceCollapsingSupply: Boolean) { CreateCollection( @@ -250,7 +250,7 @@ mutation CreateCollection($forceCollapsingSupply: Boolean) { variables = {'forceCollapsingSupply': False} #Set to true to enforce collapsing supply mint policy -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) diff --git a/docs/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md b/docs/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md index 71e9273..dc2824b 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md +++ b/docs/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md @@ -40,7 +40,7 @@ mutation SendTransferOwnershipRequest { ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation SendTransferOwnershipRequest(\r\n $collection_id: BigInt!\r\n $new_owner: String\r\n) {\r\n MutateCollection(\r\n collectionId: $collection_id\r\n mutation: { owner: $new_owner }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"new_owner":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"}}' @@ -70,7 +70,7 @@ mutateCollection.Fragment(transactionFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -88,7 +88,7 @@ Work in progress ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -123,7 +123,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation SendTransferOwnershipRequest( $collection_id: BigInt! @@ -180,7 +180,7 @@ variables = { } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -211,7 +211,7 @@ mutation AcceptTransferOwnershipRequest { ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation AcceptTransferOwnershipRequest($collection_id: BigInt!) {\r\n AcceptCollectionTransfer(collectionId: $collection_id) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105}}' @@ -236,7 +236,7 @@ acceptCollectionTransfer.Fragment(transactionFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -254,7 +254,7 @@ Work in progress ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -284,7 +284,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation AcceptTransferOwnershipRequest( $collection_id: BigInt! @@ -331,7 +331,7 @@ variables = { } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) diff --git a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md index c0c1bac..0d864db 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md +++ b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md @@ -75,7 +75,7 @@ mutation CreateCurrencyToken{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation CreateCurrencyToken(\r\n $recipient: String!\r\n $collection_id: BigInt!\r\n $token_id: BigInt\r\n $initial_supply: BigInt\r\n $cap: TokenMintCapType!\r\n $metadata: TokenMetadataInput\r\n) {\r\n CreateToken(\r\n recipient: $recipient\r\n collectionId: $collection_id\r\n params: {\r\n tokenId: { integer: $token_id }\r\n initialSupply: $initial_supply\r\n cap: { type: $cap }\r\n metadata: $metadata\r\n }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","collection_id":2406,"token_id":0,"initial_supply":1,"cap":"INFINITE","metadata":{"name":"Gold Coins","symbol":"GOLD","decimalCount":2}}}' @@ -115,7 +115,7 @@ createToken.Fragment(createTokenFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -133,7 +133,7 @@ Work in Progress! ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -181,7 +181,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation CreateCurrencyToken( $recipient: String! @@ -261,7 +261,7 @@ variables = { 'metadata': {'name': "Gold Coins", 'symbol': "GOLD", 'decimalCount': 2} #Define currency configuration here } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) diff --git a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md index eaec010..d1afccd 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md @@ -94,7 +94,7 @@ mutation CreateToken{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation CreateToken(\r\n $recipient: String!\r\n $collection_id: BigInt!\r\n $token_id: BigInt\r\n $initial_supply: BigInt\r\n $cap: TokenMintCapType!\r\n) {\r\n CreateToken(\r\n recipient: $recipient\r\n collectionId: $collection_id\r\n params: {\r\n tokenId: { integer: $token_id }\r\n initialSupply: $initial_supply\r\n cap: { type: $cap }\r\n }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","collection_id":91829,"token_id":0,"initial_supply":1,"cap":"INFINITE"}}' @@ -127,7 +127,7 @@ createToken.Fragment(createTokenFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -179,7 +179,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -221,7 +221,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -265,7 +265,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation CreateToken( $recipient: String! @@ -339,7 +339,7 @@ variables = { 'cap': 'INFINITE' #Define supply type } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) diff --git a/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md b/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md index 0d4a079..db0b751 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md +++ b/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md @@ -75,7 +75,7 @@ Ensure you use the correct casing when defining attributes to avoid errors. ### Option A. Using the Enjin Dashboard -In the Platform menu, navigate to "**[Tokens](https://platform.canary.enjin.io/tokens)**". +In the Platform menu, navigate to "**[Tokens](https://platform.beta.enjin.io/tokens)**". **Locate the token** you wish to add / edit attributes for, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Attributes**" button. ![Adding Metadata](/img/guides/managing-tokens/adding-metadata.gif) @@ -140,7 +140,7 @@ mutation BatchSetAttribute { ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation BatchSetAttribute($collection_id: BigInt!, $token_id: BigInt) {\r\n BatchSetAttribute(\r\n collectionId: $collection_id\r\n tokenId: { integer: $token_id }\r\n attributes: [\r\n { key: \"name\", value: \"Chronicles of the Celestium\" }\r\n {\r\n key: \"description\"\r\n value: \"An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse.\"\r\n }\r\n { key: \"uri\", value: \"https://yourhost/metadata.json\" }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":0}}' @@ -181,7 +181,7 @@ batchSetAttribute.Fragment(batchSetAttributeFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -244,7 +244,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -286,7 +286,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -330,7 +330,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation BatchSetAttribute($collection_id: BigInt!, $token_id: BigInt) { BatchSetAttribute( @@ -392,7 +392,7 @@ variables = { 'token_id': 0, #Specify the token ID. } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) diff --git a/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md b/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md index 9f33619..e2326c8 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md +++ b/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md @@ -29,7 +29,7 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa ## Option A. Using the Enjin Dashboard -In the Platform menu, navigate to "**[Tokens](https://platform.canary.enjin.io/tokens)**". +In the Platform menu, navigate to "**[Tokens](https://platform.beta.enjin.io/tokens)**". **Locate the token** you wish to mint, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Mint**" button. ![Minting a Token](/img/guides/managing-tokens/minting-a-token.gif) @@ -82,7 +82,7 @@ mutation BatchMint { ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation BatchMint($collection_id: BigInt!) {\r\n BatchMint(\r\n collectionId: $collection_id\r\n recipients: [\r\n {\r\n account: \"0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921\"\r\n mintParams: {\r\n amount: 1\r\n tokenId: { integer: 6533 }\r\n }\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"collection_id":7154}}' @@ -119,7 +119,7 @@ batchMint.Fragment(batchMintFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -172,7 +172,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -214,7 +214,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -253,7 +253,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation BatchMint( $collection_id: BigInt! @@ -317,7 +317,7 @@ variables = { 'collection_id': 7154 #Specify the collection ID } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) diff --git a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md index e249128..aa77da8 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md @@ -28,7 +28,7 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa ## Option A. Using the Enjin Dashboard -In the Platform menu, navigate to "**[Tokens](https://platform.canary.enjin.io/tokens)**". +In the Platform menu, navigate to "**[Tokens](https://platform.beta.enjin.io/tokens)**". **Locate the token** you wish to transfer, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Transfer**" button. ![Transferring Token](/img/guides/managing-tokens/transferring-token.gif) @@ -86,7 +86,7 @@ mutation TransferToken{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation TransferToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $recipient: String!\r\n $amount: BigInt!\r\n) {\r\n SimpleTransferToken(\r\n collectionId: $collection_id\r\n recipient: $recipient\r\n params: { tokenId: { integer: $token_id }, amount: $amount }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"collection_id":36105,"token_id":0,"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","amount":1}}' @@ -119,7 +119,7 @@ simpleTransferToken.Fragment(simpleTransferTokenFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -165,7 +165,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -208,7 +208,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -249,7 +249,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation TransferToken( $collection_id: BigInt! @@ -317,7 +317,7 @@ variables = { 'amount': 1 #Choose the transfer amount } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -347,7 +347,7 @@ mutation TransferENJTokens { ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation TransferENJTokens(\r\n $recipient: String!\r\n $amount: BigInt!\r\n $keep_alive: Boolean\r\n) {\r\n TransferBalance(\r\n recipient: $recipient\r\n amount: $amount\r\n keepAlive: $keep_alive\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","amount":1,"keep_alive":true}}' @@ -373,7 +373,7 @@ transferKeepAlive.Fragment(transferKeepAliveFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -391,7 +391,7 @@ Work In Progress ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -424,7 +424,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation TransferKeepAlive( $recipient: String! @@ -477,7 +477,7 @@ variables = { } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -530,7 +530,7 @@ mutation BatchSendENJ{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation BatchSendENJ($recipients: [TransferRecipient!]!) {\r\n BatchTransferBalance(recipients: $recipients) {\r\n id\r\n transactionId\r\n state\r\n }\r\n}\r\n","variables":{"recipients":[{"account":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","transferBalanceParams":{"value":5000000000000000000}},{"account":"cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu","transferBalanceParams":{"value":15250000000000000000,"keepAlive":true}}]}}' @@ -573,7 +573,7 @@ batchTransferBalance.Fragment(transferBalanceFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -591,7 +591,7 @@ Work In Progress ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -631,7 +631,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation BatchSendENJ($recipients: [TransferRecipient!]!){ BatchTransferBalance( @@ -697,7 +697,7 @@ variables = { ] } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) diff --git a/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md b/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md index 677b54e..93cecac 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md +++ b/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md @@ -54,7 +54,7 @@ Simply navigate to the corresponding menu for tokens instead of collections, or ### Freezing an entire collection -In the Platform menu, navigate to "**[Collections](https://platform.canary.enjin.io/collections)**". +In the Platform menu, navigate to "**[Collections](https://platform.beta.enjin.io/collections)**". **Locate the collection** you wish to freeze, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Freeze**" button. ![Freezing a Collection](/img/guides/managing-tokens/freezing-collection.gif) @@ -105,7 +105,7 @@ mutation FreezeCollection{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation FreezeCollection($collection_id: BigInt!, $freeze_type: FreezeType!) {\r\n Freeze(collectionId: $collection_id, freezeType: $freeze_type) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"freeze_type":"COLLECTION"}}' @@ -131,7 +131,7 @@ freezeCollection.Fragment(transactionFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -167,7 +167,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -208,7 +208,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -242,7 +242,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation FreezeCollection ( @@ -296,7 +296,7 @@ variables = { 'freeze_type': "COLLECTION" #For collection freezing } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -330,7 +330,7 @@ mutation FreezeToken{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation FreezeToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $freeze_type: FreezeType!\r\n $freeze_state: FreezeStateType\r\n) {\r\n Freeze(\r\n collectionId: $collection_id\r\n tokenId: { integer: $token_id }\r\n freezeType: $freeze_type\r\n freezeState: $freeze_state\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":0,"freeze_type":"TOKEN","freeze_state":"TEMPORARY"}}' @@ -358,7 +358,7 @@ freezeToken.Fragment(transactionFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -400,7 +400,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -441,7 +441,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -482,7 +482,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation FreezeToken ( @@ -550,7 +550,7 @@ variables = { 'freeze_state': "TEMPORARY" #Select the freeze state } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -588,7 +588,7 @@ mutation ThawCollection{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation ThawCollection($collection_id: BigInt!, $freeze_type: FreezeType!) {\r\n Thaw(collectionId: $collection_id, freezeType: $freeze_type) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"freeze_type":"COLLECTION"}}' @@ -614,7 +614,7 @@ thawCollection.Fragment(transactionFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -650,7 +650,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -691,7 +691,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -725,7 +725,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation ThawCollection ( @@ -779,7 +779,7 @@ variables = { 'freeze_type': "COLLECTION" #For collection thawing } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -812,7 +812,7 @@ mutation ThawToken{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation ThawToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $freeze_type: FreezeType!\r\n) {\r\n Thaw(\r\n collectionId: $collection_id\r\n tokenId: { integer: $token_id }\r\n freezeType: $freeze_type\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":0,"freeze_type":"TOKEN"}}' @@ -839,7 +839,7 @@ thawToken.Fragment(transactionFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -880,7 +880,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -922,7 +922,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -959,7 +959,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation ThawToken ( @@ -1019,7 +1019,7 @@ variables = { 'freeze_type': "TOKEN" #For token thawing } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) diff --git a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md index 55cea11..8896e7e 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md @@ -27,7 +27,7 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa ### Melting token's supply -In the Platform menu, navigate to "**[Tokens](https://platform.canary.enjin.io/tokens)**". +In the Platform menu, navigate to "**[Tokens](https://platform.beta.enjin.io/tokens)**". **Locate the token** you wish to melt, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Burn**" button. ![melting a Token](/img/guides/managing-tokens/burning-token.gif) @@ -82,7 +82,7 @@ To destroy a token, follow the above instructions for Melting a token, but make - If the collection has some tokens, you can follow the above guide [Destroying a token and removing it from the Blockchain](#destroying-a-token-and-removing-it-from-the-blockchain) for each of the tokens in the collection, to destroy them all. ::: -In the Platform menu, navigate to "**[Collections](https://platform.canary.enjin.io/collections)**". +In the Platform menu, navigate to "**[Collections](https://platform.beta.enjin.io/collections)**". **Locate the collection** you wish to destroy, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Destroy**" button. ![Destroying a Collection](/img/guides/managing-tokens/destroying-collection.gif) @@ -132,7 +132,7 @@ mutation BurnToken{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation BurnToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $amount: BigInt!\r\n) {\r\n Burn(\r\n collectionId: $collection_id\r\n params: { tokenId: { integer: $token_id }, amount: $amount }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":5,"amount":1}}' @@ -163,7 +163,7 @@ burn.Fragment(burnFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -181,7 +181,7 @@ Snippet In Progress ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -219,7 +219,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation BurnToken( $collection_id: BigInt! @@ -281,7 +281,7 @@ variables = { 'amount': 1 #Specify the amount of supply to burn } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -325,7 +325,7 @@ mutation DestroyToken{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation BurnToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $amount: BigInt!\r\n) {\r\n Burn(\r\n collectionId: $collection_id\r\n params: {\r\n tokenId: { integer: $token_id }\r\n amount: $amount\r\n removeTokenStorage: true\r\n }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":5,"amount":1}}' @@ -357,7 +357,7 @@ burn.Fragment(burnFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -375,7 +375,7 @@ Snippet In Progress ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -414,7 +414,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation BurnToken( $collection_id: BigInt! @@ -478,7 +478,7 @@ variables = { 'amount': 1 #Specify the amount of supply to burn } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -515,7 +515,7 @@ mutation DestroyCollection { ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation DestroyCollection($collection_id: BigInt!) {\r\n DestroyCollection(collectionId: $collection_id) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105}}' @@ -540,7 +540,7 @@ destroyCollection.Fragment(destrotCollectionFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -558,7 +558,7 @@ Snippet In Progress ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -586,7 +586,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation DestroyCollection($collection_id: BigInt!){ DestroyCollection( @@ -628,7 +628,7 @@ variables = { 'collection_id': 36105 #Specify the collection ID } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) diff --git a/docs/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md b/docs/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md index 22b55b8..32b5f36 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md +++ b/docs/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md @@ -56,7 +56,7 @@ query FetchingTokenHolders{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"query FetchingTokenHolders($collection_id: BigInt!, $token_id: BigInt!) {\r\n GetToken(collectionId: $collection_id, tokenId: { integer: $token_id }) {\r\n accounts {\r\n edges {\r\n node {\r\n balance\r\n wallet {\r\n account {\r\n address\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}","variables":{"collection_id":36105,"token_id":0}}' @@ -91,7 +91,7 @@ getToken.Fragment(tokenFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -141,7 +141,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -182,7 +182,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -225,7 +225,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` query FetchingTokenHolders ( @@ -297,7 +297,7 @@ variables = { 'token_id': 0 #Specify the token ID } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -380,7 +380,7 @@ query FetchingCollectionHolders{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"query FetchingCollectionHolders($collection_id: BigInt!) {\r\n GetCollection(collectionId: $collection_id) {\r\n accounts {\r\n edges {\r\n node {\r\n accountCount\r\n wallet {\r\n account {\r\n address\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}","variables":{"collection_id":36105}}' @@ -414,7 +414,7 @@ getCollection.Fragment(collectionFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -464,7 +464,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -505,7 +505,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -543,7 +543,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` query FetchingCollectionHolders ( @@ -605,7 +605,7 @@ variables = { 'collection_id': 36105 #Specify the collection ID } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) diff --git a/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md b/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md index f5e1df5..c3fbf62 100644 --- a/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md +++ b/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md @@ -111,7 +111,7 @@ At `examples/react-next/pages/api`, there's two endpoints we'll be using: - `send.tsx` which broadcasts the transaction request to the blockchain using Enjin Platform API. First, we need to insert our Enjin platform API key to be able to interact with it. -If you haven't created an api key yet, you can do so in https://platform.canary.enjin.io/settings. +If you haven't created an api key yet, you can do so in https://platform.beta.enjin.io/settings. In both `examples/react-next/pages/api/transaction.tsx` and `examples/react-next/pages/api/send.tsx` files, replace `your-api-key` with your api key (note, there's three different api key fields that needs to be updated). @@ -138,7 +138,7 @@ We are constructing the transaction using the `transaction.tsx` endpoint. First, we make a query to the user's address to get its nonce: ```javascript - const walletResponse = await fetch('https://platform.canary.enjin.io/graphql', { + const walletResponse = await fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', mode: 'no-cors', headers: { @@ -162,7 +162,7 @@ Once we get the nonce, we prepare the `CreateCollection` transaction call using You can use any other mutation Enjin Platform API offers, but for this example we're using CreateCollection. ```javascript - const response = await fetch('https://platform.canary.enjin.io/graphql', { + const response = await fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', mode: 'no-cors', headers: { @@ -209,7 +209,7 @@ We can now broadcast the signed transaction to the blockchain using the Enjin Pl This is done at: `examples/react-next/pages/api/send.tsx` ```javascript - const response = await fetch('https://platform.canary.enjin.io/graphql', { + const response = await fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', mode: 'no-cors', headers: { @@ -230,7 +230,7 @@ This is done at: `examples/react-next/pages/api/send.tsx` ``` The Enjin Platform will broadcast the transaction to the blockchain and keep track of it. -You can see the transaction status in Enjin Platform: https://platform.canary.enjin.io/transactions +You can see the transaction status in Enjin Platform: https://platform.beta.enjin.io/transactions *** diff --git a/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md b/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md index 4fc55c4..96c6582 100644 --- a/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md +++ b/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md @@ -50,7 +50,7 @@ query RequestAccount{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"query RequestAccount {\r\n RequestAccount {\r\n qrCode #Returns a QR code for your user to scan\r\n verificationId #Save this for next step\r\n }\r\n}\r\n","variables":{}}' @@ -73,7 +73,7 @@ requestAccount.Fragment(accountRequestFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -106,7 +106,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -147,7 +147,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -169,7 +169,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` query RequestAccount{ RequestAccount{ @@ -198,7 +198,7 @@ query RequestAccount{ } '''` -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -213,7 +213,7 @@ print(response.json()) { "data": { "RequestAccount": { - "qrCode": "https://platform.canary.enjin.io/qr?s=512&f=png&d=https://platform.canary.enjin.io/proof/MHg3OGFmYWIxYWQxMjk0YzFlZWY4MjdlNmFjNTM1MmJiOTVmNmFmNWJhNmUyNjk5ZGRkOTI3YjAwNmQ3ZDk0MzZjO2Vwc3I6QTIwMTBBSzlM", + "qrCode": "https://platform.beta.enjin.io/qr?s=512&f=png&d=https://platform.beta.enjin.io/proof/MHg3OGFmYWIxYWQxMjk0YzFlZWY4MjdlNmFjNTM1MmJiOTVmNmFmNWJhNmUyNjk5ZGRkOTI3YjAwNmQ3ZDk0MzZjO2Vwc3I6QTIwMTBBSzlM", "verificationId": "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" } } @@ -250,7 +250,7 @@ query GetVerifiedWallet{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"query GetVerifiedWallet($verification_id: String!) {\r\n GetWallet(verificationId: $verification_id) {\r\n account {\r\n address\r\n }\r\n balances {\r\n free\r\n }\r\n }\r\n}","variables":{"verification_id":"0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"}}' @@ -278,7 +278,7 @@ getWallet.Fragment(walletFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -317,7 +317,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -359,7 +359,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -388,7 +388,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` query GetVerifiedWallet($verification_id: String!) { GetWallet(verificationId: $verification_id) { @@ -432,7 +432,7 @@ variables = { 'verification_id': "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" #Set the verificationId from the RequestAccount response } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -496,7 +496,7 @@ query GetAccountVerified{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"query GetAccountVerified($verification_id: String!) {\r\n GetAccountVerified(verificationId: $verification_id) {\r\n verified\r\n account {\r\n publicKey\r\n address\r\n }\r\n }\r\n}","variables":{"verification_id":"0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"}}' @@ -523,7 +523,7 @@ getAccountVerified.Fragment(accountVerifiedFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -560,7 +560,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -602,7 +602,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -630,7 +630,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` query GetAccountVerified($verification_id: String!) { GetAccountVerified(verificationId: $verification_id) { @@ -672,7 +672,7 @@ variables = { 'verification_id': "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" #Set the verificationId from the RequestAccount response } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) diff --git a/docs/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md b/docs/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md index 993dc05..babf025 100644 --- a/docs/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md +++ b/docs/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md @@ -48,7 +48,7 @@ query FetchWalletBalance{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"query FetchWalletBalance($account: String!) {\r\n GetWallet(account: $account) {\r\n balances {\r\n free\r\n reserved\r\n }\r\n }\r\n}","variables":{"account":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"}}' @@ -74,7 +74,7 @@ getWallet.Fragment(walletFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -111,7 +111,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -152,7 +152,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -179,7 +179,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` query FetchWalletBalance($account: String!) { GetWallet(account: $account) { @@ -219,7 +219,7 @@ variables = { 'account': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the account address } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -279,7 +279,7 @@ query FetchingWalletCollections{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"query FetchingWalletCollections($account: String!) {\r\n GetWallet(account: $account) {\r\n collectionAccounts {\r\n edges {\r\n node {\r\n collection {\r\n collectionId\r\n attributes {\r\n key\r\n value\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}","variables":{"account":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"}}' @@ -314,7 +314,7 @@ getWallet.Fragment(walletFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -364,7 +364,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -405,7 +405,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -441,7 +441,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` query FetchingWalletCollections($account: String!) { GetWallet(account: $account) { @@ -499,7 +499,7 @@ variables = { 'account': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the account address } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -634,7 +634,7 @@ getWallet.Fragment(walletFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -689,7 +689,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -730,7 +730,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -770,7 +770,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` query FetchingWalletTokens($account: String!) { GetWallet(account: $account) { @@ -836,7 +836,7 @@ variables = { 'account': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the account address } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) diff --git a/docs/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md b/docs/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md index f6314e5..37bc5c6 100644 --- a/docs/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md +++ b/docs/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md @@ -48,7 +48,7 @@ mutation CreateManagedWallet { ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation CreateManagedWallet($external_id: String!) {\r\n CreateWallet(externalId: $external_id)\r\n}","variables":{"externalId":"player_1_id"}}' @@ -65,7 +65,7 @@ var createWallet = new CreateWallet() // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -92,7 +92,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -133,7 +133,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -155,7 +155,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation CreateManagedWallet($external_id: String!) { CreateWallet(externalId: $external_id) @@ -185,7 +185,7 @@ variables = { 'externalId': "player_1_id" #Replace this with a unique ID } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -224,7 +224,7 @@ query GetManagedWalletPublicKey { ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"query GetManagedWalletPublicKey($external_id: String!) {\r\n GetWallet(externalId: $external_id) {\r\n account {\r\n address\r\n publicKey\r\n }\r\n }\r\n}","variables":{"externalId":"player_1_id"}}' @@ -248,7 +248,7 @@ var walletFragment = new WalletFragment() // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -284,7 +284,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -325,7 +325,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -352,7 +352,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` query GetManagedWalletPublicKey($external_id: String!) { GetWallet(externalId: $external_id) { @@ -392,7 +392,7 @@ variables = { 'externalId': "player_1_id" #Specify the managed wallet unique ID } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -445,7 +445,7 @@ mutation BatchMint { ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation BatchMint($collection_id: BigInt!) {\r\n BatchMint(\r\n collectionId: $collection_id\r\n recipients: [\r\n {\r\n account: \"cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT\" #The recipient of the mint (the Managed wallet account address from the GetWallet query)\r\n mintParams: {\r\n amount: 1 #Amount to mint\r\n tokenId: { integer: 6533 } #Token ID to mint\r\n }\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":7154}}' @@ -483,7 +483,7 @@ batchMint.Fragment(transactionFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -533,7 +533,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -574,7 +574,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -611,7 +611,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation BatchMint($collection_id: BigInt!) { BatchMint( @@ -671,7 +671,7 @@ variables = { 'collection_id': 7154 #Specify the managed wallet unique ID } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -714,7 +714,7 @@ mutation BatchTransfer { ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation BatchTransfer($collection_id: BigInt!, $signing_account: String!) {\r\n BatchTransfer(\r\n collectionId: $collection_id\r\n signingAccount: $signing_account\r\n recipients: [\r\n {\r\n account: \"cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ\" #The recipient of the transfer\r\n simpleParams: {\r\n tokenId: { integer: 6533 } #Token ID to transfer\r\n amount: 1 #Amount to transfer\r\n }\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":7154,"signing_account":"cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT"}}' @@ -752,7 +752,7 @@ batchTransfer.Fragment(transactionFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -803,7 +803,7 @@ int main() { // Create and auth a client to send the request to the platform unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client->Auth("Your_Platform_Token_Here"); @@ -844,7 +844,7 @@ int main() { ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -887,7 +887,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation BatchTransfer ( @@ -959,7 +959,7 @@ variables = { 'signing_account': "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" #Add your signing account address (the Managed wallet account address from the GetWallet query) } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) diff --git a/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md b/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md index 5ecaa9c..be3c762 100644 --- a/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md +++ b/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md @@ -91,7 +91,7 @@ Now that we have a basic understanding of how Fuel Tanks are structured, let's g This guide will demonstrate how to create various fuel tanks with the GraphQL API. Note that you may also use the User Interface to create Fuel Tanks. -To create a Fuel Tank using the Platform's User Interface, navigate to "**[Fuel Tanks](https://platform.canary.enjin.io/fuel-tanks)**" in the Platform Menu. Then, click the "**[Create Fuel Tank](https://platform.canary.enjin.io/create/fuel-tank)**" button. +To create a Fuel Tank using the Platform's User Interface, navigate to "**[Fuel Tanks](https://platform.beta.enjin.io/fuel-tanks)**" in the Platform Menu. Then, click the "**[Create Fuel Tank](https://platform.beta.enjin.io/create/fuel-tank)**" button. ::: :::warning **Feature Availability Notice** @@ -103,12 +103,10 @@ We will update the documentation once these options are available. Code snippets with these arguments are provided for illustrative purposes only. If you wish to create a fuel tank with these options now, please use the Enjin Console at [console.enjin.io](https://console.enjin.io). ::: -:::info Make sure to use the Fuel Tank Endpoint -- Testnet: `http://platform.canary.enjin.io/graphql/fuel-tanks` -- Mainnet: `http://platform.beta.enjin.io/graphql/fuel-tanks` -**Try the Beam Playground here:** -- Testnet: http://platform.canary.enjin.io/graphiql/fuel-tanks -- Mainnet: http://platform.beta.enjin.io/graphiql/fuel-tanks +:::info GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` + +**Try the GraphiQL Playground here:** https://platform.beta.enjin.io/graphiql ::: ### Subsidize Token Transfers For A Collection @@ -137,7 +135,7 @@ mutation CreateFuelTank{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation CreateFuelTank(\r\n $name: String!\r\n $coveragePolicy: CoveragePolicy\r\n $permittedExtrinsics: [TransactionMethod!]\r\n $whitelistedCollections: [BigInt!]\r\n) {\r\n CreateFuelTank(\r\n name: $name\r\n coveragePolicy: $coveragePolicy\r\n dispatchRules: [\r\n {\r\n permittedExtrinsics: $permittedExtrinsics #This rule specifies that only batch transfers are subsidized\r\n whitelistedCollections: $whitelistedCollections #This rule ensures that only the specified collection is subsidized\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"name":"Collection Token Transfers","coveragePolicy":"FEES","permittedExtrinsics":["BatchTransfer"],"whitelistedCollections":[36105]}}' @@ -174,7 +172,7 @@ createFuelTank.Fragment(transactionFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -192,7 +190,7 @@ Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -234,7 +232,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation CreateFuelTank ( @@ -304,7 +302,7 @@ variables = { 'whitelistedCollections': [36105] #This rule ensures that only the specified collection is subsidized } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -346,7 +344,7 @@ mutation CreateFuelTank{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation CreateFuelTank(\r\n $name: String!\r\n $coveragePolicy: CoveragePolicy\r\n $reserveAccountCreationDeposit: Boolean\r\n $whitelistedPallets: [String!]\r\n $whitelistedCollections: [BigInt!]\r\n) {\r\n CreateFuelTank(\r\n name: $name\r\n coveragePolicy: $coveragePolicy\r\n reservesAccountCreationDeposit: $reserveAccountCreationDeposit\r\n dispatchRules: [\r\n {\r\n whitelistedPallets: $whitelistedPallets\r\n whitelistedCollections: $whitelistedCollections\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"name":"Collection Token Actions","coveragePolicy":"FEES_AND_DEPOSIT","reserveAccountCreationDeposit":true,"whitelistedPallets":["MultiTokens","Marketplace"],"whitelistedCollections":[36105]}}' @@ -383,7 +381,7 @@ createFuelTank.Fragment(transactionFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -401,7 +399,7 @@ Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -447,7 +445,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation CreateFuelTank( $name: String! @@ -525,7 +523,7 @@ variables = { 'whitelistedCollections': [36105] #This rule ensures that only the specified collection is subsidized } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -575,7 +573,7 @@ mutation CreateFuelTank{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation CreateFuelTank(\r\n $name: String!\r\n $coveragePolicy: CoveragePolicy\r\n $whitelistedCallers: [String!]\r\n $requireAccount: Boolean\r\n $userFuelBudget: FuelBudgetInputType\r\n) {\r\n CreateFuelTank(\r\n name: $name\r\n coveragePolicy: $coveragePolicy\r\n accountRules: { whitelistedCallers: $whitelistedCallers }\r\n dispatchRules: [\r\n { requireAccount: $requireAccount, userFuelBudget: $userFuelBudget }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"name":"Only Specific Accounts Allowed","coveragePolicy":"FEES","whitelistedCallers":["cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu","cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP"],"requireAccount":true,"userFuelBudget":{"amount":5000000000000000000,"resetPeriod":432000}}}' @@ -626,7 +624,7 @@ createFuelTank.Fragment(transactionFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -644,7 +642,7 @@ Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -693,7 +691,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation CreateFuelTank( $name: String! @@ -777,7 +775,7 @@ variables = { } } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) @@ -862,7 +860,7 @@ mutation Dispatch { ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation Dispatch(\r\n $tankId: String!\r\n $ruleSetId: BigInt!\r\n $call: DispatchCall!\r\n $query: String!\r\n $variables: Object\r\n) {\r\n Dispatch(\r\n tankId: $tankId\r\n ruleSetId: $ruleSetId\r\n dispatch: { call: $call, query: $query, variables: $variables }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"tankId":"efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou","ruleSetId":0,"call":"MULTI_TOKENS","query":"mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}","variables":null}}' @@ -896,7 +894,7 @@ dispatch.Fragment(transactionFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -914,7 +912,7 @@ Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -954,7 +952,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation Dispatch( $tankId: String! @@ -1020,7 +1018,7 @@ variables = { 'variables': None #If the mutation has variables, insert them here as an object. } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) diff --git a/docs/02-guides/01-platform/03-advanced-mechanics/04-brand-collab-nfts.md b/docs/02-guides/01-platform/03-advanced-mechanics/04-brand-collab-nfts.md index 75f6b49..c73f813 100644 --- a/docs/02-guides/01-platform/03-advanced-mechanics/04-brand-collab-nfts.md +++ b/docs/02-guides/01-platform/03-advanced-mechanics/04-brand-collab-nfts.md @@ -111,7 +111,7 @@ tokenAccounts (collectionIds:2100 tokenIds:1 after:"" first:100) { ``` -curl -X POST "https://platform.canary.enjin.io/graphql" \ +curl -X POST "https://platform.beta.enjin.io/graphql" \ -H "Content-Type: application/json" \ -H "Authorization: enjin_api_key" \ -d '{ diff --git a/docs/02-guides/01-platform/03-advanced-mechanics/05-enj-infusion.md b/docs/02-guides/01-platform/03-advanced-mechanics/05-enj-infusion.md index cabf186..de0956c 100644 --- a/docs/02-guides/01-platform/03-advanced-mechanics/05-enj-infusion.md +++ b/docs/02-guides/01-platform/03-advanced-mechanics/05-enj-infusion.md @@ -85,7 +85,7 @@ mutation Infuse{ ``` -curl --location 'https://platform.canary.enjin.io/graphql' \ +curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: enjin_api_key' \ -d '{"query":"mutation Infuse(\r\n $collection_id: BigInt!\r\n $token_id: EncodableTokenIdInput!\r\n $amount: BigInt!\r\n) {\r\n Infuse(collectionId: $collection_id, tokenId: $token_id, amount: $amount) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":3298,"token_id":{"integer":1},"amount":5000000000000000000}}' @@ -112,7 +112,7 @@ infuse.Fragment(transactionFragment); // Create and auth a client to send the request to the platform var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") + .SetBaseAddress("https://platform.beta.enjin.io") .Build(); client.Auth("Your_Platform_Token_Here"); @@ -130,7 +130,7 @@ Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); ```javascript -fetch('https://platform.canary.enjin.io/graphql', { +fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ @@ -162,7 +162,7 @@ fetch('https://platform.canary.enjin.io/graphql', { ```javascript const axios = require('axios'); -axios.post('https://platform.canary.enjin.io/graphql', { +axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation Infuse( $collection_id: BigInt! @@ -212,7 +212,7 @@ variables = { 'amount': 5000000000000000000 #Specify the amount of ENJ to infuse } -response = requests.post('https://platform.canary.enjin.io/graphql', +response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} ) diff --git a/docs/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md b/docs/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md index 592b52c..0e57249 100644 --- a/docs/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md +++ b/docs/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md @@ -29,7 +29,7 @@ Before you begin, make sure you have the following installed: - ✅ **Unity Hub** with **Unity Editor version `6000.0.24f1`**. - ✅ **Node.js** (which includes `npm`). - ✅ **Git** for cloning the repositories. - - ✅ An **Enjin Platform account**. If you don't have one, you can create it [here](https://platform.canary.enjin.io/). + - ✅ An **Enjin Platform account**. If you don't have one, you can create it [here](https://platform.beta.enjin.io/). - ✅ Some cENJ tokens (can be aquired from the [cENJ Faucet](https://faucet.canary.enjin.io/)) ----- @@ -60,8 +60,8 @@ Next, you'll set up your Enjin Platform account and the Wallet Daemon. ### Enjin Platform -1. Log in to your [Enjin Platform](https://platform.canary.enjin.io/) account. -2. Head over to your [account settings page](https://platform.canary.enjin.io/settings). +1. Log in to your [Enjin Platform](https://platform.beta.enjin.io/) account. +2. Head over to your [account settings page](https://platform.beta.enjin.io/settings). 3. Navigate to the **Daemon Wallet** section and create a new API Token. 4. Copy the **API Token**; you will need this in the next step. diff --git a/docs/03-api-reference/01-queries/01-transactions-queries.md b/docs/03-api-reference/01-queries/01-transactions-queries.md index 1a9c314..8a4b3d0 100644 --- a/docs/03-api-reference/01-queries/01-transactions-queries.md +++ b/docs/03-api-reference/01-queries/01-transactions-queries.md @@ -13,9 +13,8 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere 🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. ::: -:::tip Core Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.beta.enjin.io/graphql` +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` ::: This is an overview of some of the most commonly used operations in the Enjin Platform Schema. diff --git a/docs/03-api-reference/01-queries/02-collections-queries.md b/docs/03-api-reference/01-queries/02-collections-queries.md index 3b16d08..1c9b000 100644 --- a/docs/03-api-reference/01-queries/02-collections-queries.md +++ b/docs/03-api-reference/01-queries/02-collections-queries.md @@ -12,9 +12,8 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere 🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. ::: -:::tip Core Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.beta.enjin.io/graphql` +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/01-queries/03-tokens-queries.md b/docs/03-api-reference/01-queries/03-tokens-queries.md index d061300..2a41750 100644 --- a/docs/03-api-reference/01-queries/03-tokens-queries.md +++ b/docs/03-api-reference/01-queries/03-tokens-queries.md @@ -12,9 +12,8 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere 🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. ::: -:::tip Core Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.beta.enjin.io/graphql` +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/01-queries/04-wallets-queries.md b/docs/03-api-reference/01-queries/04-wallets-queries.md index 132db70..9776fcb 100644 --- a/docs/03-api-reference/01-queries/04-wallets-queries.md +++ b/docs/03-api-reference/01-queries/04-wallets-queries.md @@ -12,9 +12,8 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere 🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. ::: -:::tip Core Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.beta.enjin.io/graphql` +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/01-queries/05-fuel-tank-queries.md b/docs/03-api-reference/01-queries/05-fuel-tank-queries.md index 2555ad0..f45b694 100644 --- a/docs/03-api-reference/01-queries/05-fuel-tank-queries.md +++ b/docs/03-api-reference/01-queries/05-fuel-tank-queries.md @@ -12,9 +12,8 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere 🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. ::: -:::tip Fuel Tanks Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql/fuel-tanks` -- **Mainnet:** `http://platform.beta.enjin.io/graphql/fuel-tanks` +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/01-queries/06-marketplace-queries.md b/docs/03-api-reference/01-queries/06-marketplace-queries.md index 1b20481..aa9870f 100644 --- a/docs/03-api-reference/01-queries/06-marketplace-queries.md +++ b/docs/03-api-reference/01-queries/06-marketplace-queries.md @@ -12,9 +12,8 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere 🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. ::: -:::tip Marketplace Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql/marketplace` -- **Mainnet:** `http://platform.beta.enjin.io/graphql/marketplace` +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/01-queries/07-beam-queries.md b/docs/03-api-reference/01-queries/07-beam-queries.md index ba8702e..351e22b 100644 --- a/docs/03-api-reference/01-queries/07-beam-queries.md +++ b/docs/03-api-reference/01-queries/07-beam-queries.md @@ -13,9 +13,8 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere 🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. ::: -:::tip Beam Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql/beam` -- **Mainnet:** `http://platform.beta.enjin.io/graphql/beam` +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/02-mutations/01-transaction-mutations.md b/docs/03-api-reference/02-mutations/01-transaction-mutations.md index caba563..225cc67 100644 --- a/docs/03-api-reference/02-mutations/01-transaction-mutations.md +++ b/docs/03-api-reference/02-mutations/01-transaction-mutations.md @@ -12,9 +12,8 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere 🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. ::: -:::tip Core Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.beta.enjin.io/graphql` +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` ::: This is an overview of some of the most commonly used operations in the Enjin Platform Schema. diff --git a/docs/03-api-reference/02-mutations/02-collections-mutations.md b/docs/03-api-reference/02-mutations/02-collections-mutations.md index c4e946a..ca83a88 100644 --- a/docs/03-api-reference/02-mutations/02-collections-mutations.md +++ b/docs/03-api-reference/02-mutations/02-collections-mutations.md @@ -12,9 +12,8 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere 🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. ::: -:::tip Core Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.beta.enjin.io/graphql` +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/02-mutations/03-tokens-mutations.md b/docs/03-api-reference/02-mutations/03-tokens-mutations.md index c077d0d..73bd2f6 100644 --- a/docs/03-api-reference/02-mutations/03-tokens-mutations.md +++ b/docs/03-api-reference/02-mutations/03-tokens-mutations.md @@ -13,9 +13,8 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere 🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. ::: -:::tip Core Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.beta.enjin.io/graphql` +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/02-mutations/04-wallets-mutations.md b/docs/03-api-reference/02-mutations/04-wallets-mutations.md index 8c9e3c0..278123d 100644 --- a/docs/03-api-reference/02-mutations/04-wallets-mutations.md +++ b/docs/03-api-reference/02-mutations/04-wallets-mutations.md @@ -12,9 +12,8 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere 🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. ::: -:::tip Core Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.beta.enjin.io/graphql` +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/02-mutations/05-fuel-tanks-mutations.md b/docs/03-api-reference/02-mutations/05-fuel-tanks-mutations.md index 482f20b..9e88c3a 100644 --- a/docs/03-api-reference/02-mutations/05-fuel-tanks-mutations.md +++ b/docs/03-api-reference/02-mutations/05-fuel-tanks-mutations.md @@ -12,9 +12,8 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere 🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. ::: -:::tip Fuel Tanks Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql/fuel-tanks` -- **Mainnet:** `http://platform.beta.enjin.io/graphql/fuel-tanks` +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/02-mutations/06-marketplace-mutations.md b/docs/03-api-reference/02-mutations/06-marketplace-mutations.md index b07e3c8..75f4a4c 100644 --- a/docs/03-api-reference/02-mutations/06-marketplace-mutations.md +++ b/docs/03-api-reference/02-mutations/06-marketplace-mutations.md @@ -12,9 +12,8 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere 🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. ::: -:::tip Marketplace Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql/marketplace` -- **Mainnet:** `http://platform.beta.enjin.io/graphql/marketplace` +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/02-mutations/07-beam-mutations.md b/docs/03-api-reference/02-mutations/07-beam-mutations.md index 36afbd9..d446966 100644 --- a/docs/03-api-reference/02-mutations/07-beam-mutations.md +++ b/docs/03-api-reference/02-mutations/07-beam-mutations.md @@ -12,9 +12,8 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere 🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. ::: -:::tip Beam Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql/beam` -- **Mainnet:** `http://platform.beta.enjin.io/graphql/beam` +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` ::: This is a detailed reference guide that explains the most commonly used operations. diff --git a/docs/03-api-reference/03-api-reference.md b/docs/03-api-reference/03-api-reference.md index 0c5a327..2256a6b 100644 --- a/docs/03-api-reference/03-api-reference.md +++ b/docs/03-api-reference/03-api-reference.md @@ -10,10 +10,7 @@ The GraphiQL Playground is an interactive environment where developers can explo ### Accessing the GraphiQL Playground -You can access the GraphiQL Playground for both Testnet and Mainnet environments: - -- **Testnet**: [Core Operations](https://platform.canary.enjin.io/graphiql) -- **Mainnet**: [Core Operations](https://platform.beta.enjin.io/graphiql) +You can access the GraphiQL Playground here: [Core Operations](https://platform.beta.enjin.io/graphiql). ### Navigating the Documentation Explorer From 96ee3e418e02b879b030e9dfa6caf39e152e6371 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Tue, 12 May 2026 21:13:12 +0300 Subject: [PATCH 08/67] Drop "Canary Platform" framing from quick-start-guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is no separate Canary Platform in v3 — one Enjin Platform Cloud covers both networks via the in-UI network selector. Kept references to the Canary testnet (the chain) intact since that's still a valid v3 concept; only the "Canary " wording was changed. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/01-getting-started/02-quick-start-guide.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/01-getting-started/02-quick-start-guide.md b/docs/01-getting-started/02-quick-start-guide.md index cc4ad58..1091593 100644 --- a/docs/01-getting-started/02-quick-start-guide.md +++ b/docs/01-getting-started/02-quick-start-guide.md @@ -12,7 +12,7 @@ Welcome! This guide provides the essential steps to quickly integrate the . +- Set up your developer account on the . - Connected your developer wallet to authorize actions. - Created your first blockchain and . - Learned how to link your users' s to your application. @@ -20,11 +20,11 @@ Welcome! This guide provides the essential steps to quickly integrate the Date: Wed, 13 May 2026 14:11:05 +0300 Subject: [PATCH 09/67] Migrate Creating Collections page to v3 - Convert CreateCollection mutation to v3 CreateTransaction(createCollection) shape with uuid/action/state response and Bearer auth - Update dashboard UI bullets to match v3 form (Royalties, Explicit Royalty Currencies, Max Token Count/Supply, Force Collapsing Supply, Attributes) - Replace Wallet Daemon vs Polkadot.js signing callout with v3 Signing Account guidance (Wallet Daemon default, Managed Wallet override) - Drop v2 gif and screenshots - Add 'Platform v3 SDKs not yet available' callout above the SDK tabs; C#/C++ SDK tab bodies kept as-is until v3 SDKs ship - Drop the dead WebSocket-event line (v3 has no subscriptions) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../01-creating-collections.md | 105 ++++++++++-------- 1 file changed, 57 insertions(+), 48 deletions(-) diff --git a/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md b/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md index b157c6a..2b28e53 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md +++ b/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md @@ -25,48 +25,50 @@ On Enjin Blockchain, every token must be placed inside a ), and Attributes. - -- **[Mint Policy](/03-api-reference/04-important-arguments.md#mintpolicy) -** The rules pertaining to token supply and number of tokens available to be minted in the future. -- **[Market Policy](/03-api-reference/04-important-arguments.md#marketpolicy) -** Determines the rules which tokens in this collection must follow when interacting with the on-chain marketplace. +- **[Royalties](/03-api-reference/04-important-arguments.md#marketpolicy) -** Configure a beneficiary and percentage for marketplace sales of tokens in this collection. - **[Explicit Royalty Currencies](/03-api-reference/04-important-arguments.md#explicitroyaltycurrencies) -** Choose which currencies are required to pay marketplace royalties for the tokens in this collection. +- **[Max Token Count / Max Token Supply](/03-api-reference/04-important-arguments.md#mintpolicy) -** Optional caps on how many distinct tokens, and how much supply per token, this collection allows. +- **Force Collapsing Supply -** When enabled, burning a token reduces its max supply (burned tokens cannot be re-minted). Leave disabled for the standard behavior. - **[Attributes](/03-api-reference/04-important-arguments.md#attributes) -** Add details to your collection using key-value pairs. Standard keys like `name` and `description` allow applications to display your content correctly. You can also use the `URI` to link to a JSON file hosting your metadata. For more information, see the [Metadata Standard](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md) page. :::info Learn more about the arguments For a comprehensive view and detail of all available arguments please refer to our [API Reference](/03-api-reference/03-api-reference.md). ::: -Once you're satisfied with the options, click on the "**Create**" button at the bottom right corner to create the request. - -The Transaction Request will then appear in the "**Transactions**" menu. - -

- Collection created banner that appears after creating a transaction request -

+Once you're satisfied with the options, click on the "**Create**" button at the bottom right corner to submit the request. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. -![Pending create collection transaction](/img/guides/managing-tokens/pending-create-collection-txn.png) +Since this request requires a , it must be signed before it broadcasts. -Since this request requires a , it'll need to be signed with your Wallet. - -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. +- By default, transactions are signed automatically by the **Wallet Daemon**. +- To sign with a different account, expand **Transaction Options → Signing Account** on the form and provide a [Managed Wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) address. Once you've created a collection you're ready to start [creating tokens](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md). ## Option B. Using the Enjin API & SDKs -The `CreateCollection` mutation is used to create a new on-chain collection. +In v3, every on-chain action goes through the single `CreateTransaction` mutation. The action itself is selected by which field you set on the `transaction` input — for collections, that's `createCollection`. + +:::warning Platform v3 SDKs are not yet available +The C# and C++ SDK examples below still target the Enjin Platform v2 API and **will not work against v3**. This section will be updated once the v3 SDKs ship. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +::: ```graphql mutation CreateCollection { - CreateCollection(mintPolicy: { forceCollapsingSupply: false }) #Set to true to enforce collapsing supply mint policy - { - id - method + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + createCollection: { + forceCollapsingSupply: false # set to true to enforce collapsing supply + } + } + ) { + uuid + action state } } @@ -76,8 +78,8 @@ mutation CreateCollection { ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation CreateCollection($forceCollapsingSupply: Boolean) {\r\n CreateCollection(\r\n mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"forceCollapsingSupply":false}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation CreateCollection($forceCollapsingSupply: Boolean!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: { createCollection: { forceCollapsingSupply: $forceCollapsingSupply } }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"forceCollapsingSupply":false}}' ``` @@ -184,21 +186,23 @@ int main() { ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` - mutation CreateCollection($forceCollapsingSupply: Boolean) { - CreateCollection( - mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } + mutation CreateCollection($forceCollapsingSupply: Boolean!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { createCollection: { forceCollapsingSupply: $forceCollapsingSupply } } ) { - id - method + uuid + action state } } `, variables: { - forceCollapsingSupply: false //Set to true to enforce collapsing supply mint policy + forceCollapsingSupply: false // set to true to enforce collapsing supply } }), }) @@ -212,21 +216,23 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation CreateCollection($forceCollapsingSupply: Boolean) { - CreateCollection( - mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } + mutation CreateCollection($forceCollapsingSupply: Boolean!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { createCollection: { forceCollapsingSupply: $forceCollapsingSupply } } ) { - id - method + uuid + action state } } `, variables: { - forceCollapsingSupply: false //Set to true to enforce collapsing supply mint policy + forceCollapsingSupply: false // set to true to enforce collapsing supply } }, { - headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN' } }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -237,34 +243,37 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation CreateCollection($forceCollapsingSupply: Boolean) { - CreateCollection( - mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } +mutation CreateCollection($forceCollapsingSupply: Boolean!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { createCollection: { forceCollapsingSupply: $forceCollapsingSupply } } ) { - id - method + uuid + action state } } ''' -variables = {'forceCollapsingSupply': False} #Set to true to enforce collapsing supply mint policy +variables = {'forceCollapsingSupply': False} # set to true to enforce collapsing supply response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` +The response includes the transaction's `uuid`, `action` (e.g. `MultiTokens.create_collection`), and `state` (`PENDING` → `BROADCAST` → `FINALIZED`). Use `GetTransaction(network, chain, uuid: "")` to poll the current state. + :::info Explore More Arguments For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -For instance, you'll find settings such as different supply types with the `MintPolicy` argument, enforcing royalties with the `MarketPolicy` argument, or adding metadata with the `AttributeInput` argument. -::: -A WebSocket event will also be fired so you can pick up the collection in real time by listening to the app channel on the WebSocket. +In addition to `forceCollapsingSupply`, `createCollection` accepts `maxTokenCount`, `maxTokenSupply`, `royalties`, `explicitRoyaltyCurrencies`, and `attributes`. +::: :::tip What's next? You've created a collection, now [Fill it with Tokens](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) From 9e0dea9bfc7c83b4b8923cba624ac7ff716cdce6 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 13 May 2026 14:12:52 +0300 Subject: [PATCH 10/67] Migrate Creating Tokens page to v3 - Convert CreateToken mutation to v3 CreateTransaction(createToken) shape with flat tokenId (BigInt scalar, no integer wrapper) and Bearer auth - Re-describe supply types: only SUPPLY and COLLAPSING_SUPPLY exist in v3; unlimited supply is now expressed by omitting the cap field entirely (no more INFINITE enum) - Add NON_NULL fields the v3 input requires (listingForbidden, infusion, anyoneCanInfuse) - Replace Wallet Daemon vs Polkadot.js callout with v3 Signing Account guidance - Drop v2 gif and screenshots - Add 'Platform v3 SDKs not yet available' warning above the SDK tabs - Drop the dead WebSocket-event paragraph Co-Authored-By: Claude Opus 4.7 (1M context) --- .../02-creating-tokens/02-creating-tokens.md | 202 ++++++++++-------- 1 file changed, 109 insertions(+), 93 deletions(-) diff --git a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md index d1afccd..a5e7e0b 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md @@ -35,58 +35,61 @@ Before minting the Mainnet versions of your Tokens, that will be used in your li In the Platform menu, navigate to "**[Tokens](https://platform.beta.enjin.io/tokens)**". Then, click the "**[Create Token](https://platform.beta.enjin.io/create/token)**" button. -![Creating Token](/img/guides/managing-tokens/creating-token.gif) +From here, you can configure the token's basic settings, supply cap, royalty behavior, and attributes. -From here, you can customize your collection's Mint Policy, Market Policy, and Attributes. - -- **Create Token Section -** Basic token options. Make sure to select the Collection ID you wish to mint the token in, the token ID, and the recipient in the corresponding fields. +- **Create Token Section -** Basic token options. Make sure to select the Collection ID you wish to mint the token in, the token ID, the initial supply, and the recipient in the corresponding fields. Make sure to check out the [TokenID Structure Best Practices](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md). -- **[Cap](/03-api-reference/04-important-arguments.md#cap) -** The token cap (if required). - - The **[Infinite](/03-api-reference/04-important-arguments.md#infinite)** supply type is the most flexible. With this model, there is no limit to how many tokens can be minted or be in circulation. The collection owner can always mint additional units, making it ideal for use cases that require an ever-expanding token supply. - - The **[Fixed](/03-api-reference/04-important-arguments.md#supply)** supply type offers a balanced approach between flexibility and control. This model allows the collection owner to mint new tokens as long as the circulating supply does not exceed the predetermined max supply. Burned tokens can be re-minted, ensuring the total supply remains constant. - - The **[Collapsing](/03-api-reference/04-important-arguments.md#collapsing_supply)** supply type is the most strict. This supply type allows the collection owner to mint new tokens as long as the circulating supply does not exceed the max supply. However, burning tokens reduces the max supply, meaning burned tokens cannot be re-minted. This ensures a non-increasing supply, suitable for use cases that require strict control over the token's total amount in circulation. -- **[Token Royalty Settings](/03-api-reference/04-important-arguments.md#hasroyalty) -** Configures royalties for each marketplace sale of this token. +- **[Cap](/03-api-reference/04-important-arguments.md#cap) -** The token cap (if required). v3 supports two cap modes — leave the cap field unset for an unlimited supply. + - **Unlimited supply (no cap)** is the default. With this model, there is no limit to how many tokens can be minted or be in circulation. The collection owner can always mint additional units, making it ideal for use cases that require an ever-expanding token supply. To pick this option, simply omit the cap field in the API, or leave the cap option unset in the dashboard form. + - The **[SUPPLY](/03-api-reference/04-important-arguments.md#supply)** cap mode offers a balanced approach between flexibility and control. This model allows the collection owner to mint new tokens as long as the circulating supply does not exceed the predetermined `supply` value. Burned tokens can be re-minted, ensuring the total supply remains constant. + - The **[COLLAPSING_SUPPLY](/03-api-reference/04-important-arguments.md#collapsing_supply)** cap mode is the most strict. This cap allows the collection owner to mint new tokens as long as the circulating supply does not exceed the `supply` value. However, burning tokens reduces the cap, meaning burned tokens cannot be re-minted. This ensures a non-increasing supply, suitable for use cases that require strict control over the token's total amount in circulation. +- **[Royalty Behavior](/03-api-reference/04-important-arguments.md#hasroyalty) -** Configures royalties for each marketplace sale of this token. v3 represents this via the `behavior` field on the token — set `behavior: { type: HAS_ROYALTY, royalties: [...] }`. - **[Attributes](/03-api-reference/04-important-arguments.md#attributes) -** Add details to your token using key-value pairs. Standard keys like `name` and `description` allow applications to display your content correctly. You can also use the `URI` to link to a JSON file hosting your metadata. For more information, see the [Metadata Standard](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md) page. :::info Learn more about the arguments For a comprehensive view and detail of all available arguments please refer to our [API Reference](/03-api-reference/03-api-reference.md). ::: -Once you're satisfied with the options, click on the "**Create**" button at the bottom right corner to create the request. - -The Transaction Request will then appear in the "**Transactions**" menu. - -

- Create Token Transaction Request Banner -

- -![Pending Create Token Transaction](/img/guides/managing-tokens/pending-create-token-txn.png) +Once you're satisfied with the options, click on the "**Create**" button at the bottom right corner to submit the request. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. -Since this request requires a , it'll need to be signed with your Wallet. +Since this request requires a , it must be signed before it broadcasts. -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. +- By default, transactions are signed automatically by the **Wallet Daemon**. +- To sign with a different account, expand **Transaction Options → Signing Account** on the form and provide a [Managed Wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) address. Once your token is created, lets give it a new look by [Adding Metadata](/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md) ## Option B. Using the Enjin API & SDKs -CreateToken mutation enables you to create a new token within an existing collection. This operation is essential for introducing new digital assets, and it allows you to define various attributes and characteristics for the newly created token. +In v3, all on-chain actions go through the single `CreateTransaction` mutation. For creating a token, set the `createToken` field on the `transaction` input. `tokenId` is now a flat `BigInt` (no `{ integer: N }` wrapper), and `cap` is optional — omit it to create a token with unlimited supply. + +:::warning Platform v3 SDKs are not yet available +The C# and C++ SDK examples below still target the Enjin Platform v2 API and **will not work against v3**. This section will be updated once the v3 SDKs ship. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +::: ```graphql -mutation CreateToken{ - CreateToken( - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #The recipient of the initial supply - collectionId: 2406 #Set the collection ID - params:{ - tokenId: {integer: 0} #Set the token ID - initialSupply: 1 #Mint initial supply - cap: {type: INFINITE}} #Define supply type +mutation CreateToken { + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + createToken: { + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" # recipient of the initial supply + collectionId: 2406 # collection to mint into + tokenId: 0 # the new token ID + initialSupply: 1 # initial supply to mint + listingForbidden: false # set true to disallow marketplace listings + infusion: 0 # ENJ infused per unit (use BigInt base units) + anyoneCanInfuse: false + # cap: omitted for unlimited supply + # for a finite cap, use: cap: { type: SUPPLY, supply: 100 } + } + } ) { - id - method + uuid + action state } } @@ -96,8 +99,8 @@ mutation CreateToken{ ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation CreateToken(\r\n $recipient: String!\r\n $collection_id: BigInt!\r\n $token_id: BigInt\r\n $initial_supply: BigInt\r\n $cap: TokenMintCapType!\r\n) {\r\n CreateToken(\r\n recipient: $recipient\r\n collectionId: $collection_id\r\n params: {\r\n tokenId: { integer: $token_id }\r\n initialSupply: $initial_supply\r\n cap: { type: $cap }\r\n }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","collection_id":91829,"token_id":0,"initial_supply":1,"cap":"INFINITE"}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation CreateToken($recipient: String!, $collectionId: BigInt!, $tokenId: BigInt!, $initialSupply: BigInt!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: {\r\n createToken: {\r\n recipient: $recipient\r\n collectionId: $collectionId\r\n tokenId: $tokenId\r\n initialSupply: $initialSupply\r\n listingForbidden: false\r\n infusion: 0\r\n anyoneCanInfuse: false\r\n }\r\n }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","collectionId":2406,"tokenId":0,"initialSupply":1}}' ``` @@ -223,37 +226,41 @@ int main() { ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` mutation CreateToken( $recipient: String! - $collection_id: BigInt! - $token_id: BigInt - $initial_supply: BigInt - $cap: TokenMintCapType! + $collectionId: BigInt! + $tokenId: BigInt! + $initialSupply: BigInt! ) { - CreateToken( - recipient: $recipient - collectionId: $collection_id - params: { - tokenId: { integer: $token_id } - initialSupply: $initial_supply - cap: { type: $cap } + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + createToken: { + recipient: $recipient + collectionId: $collectionId + tokenId: $tokenId + initialSupply: $initialSupply + listingForbidden: false + infusion: 0 + anyoneCanInfuse: false + } } ) { - id - method + uuid + action state } } `, variables: { - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //The recipient of the initial supply - collection_id: 2406, //Specify the collection ID - token_id: 0, //Specify the token ID - initial_supply: 1, //Mint initial supply - cap: "INFINITE" //Define supply type + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", + collectionId: 2406, + tokenId: 0, + initialSupply: 1 } }), }) @@ -269,35 +276,39 @@ axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation CreateToken( $recipient: String! - $collection_id: BigInt! - $token_id: BigInt - $initial_supply: BigInt - $cap: TokenMintCapType! + $collectionId: BigInt! + $tokenId: BigInt! + $initialSupply: BigInt! ) { - CreateToken( - recipient: $recipient - collectionId: $collection_id - params: { - tokenId: { integer: $token_id } - initialSupply: $initial_supply - cap: { type: $cap } + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + createToken: { + recipient: $recipient + collectionId: $collectionId + tokenId: $tokenId + initialSupply: $initialSupply + listingForbidden: false + infusion: 0 + anyoneCanInfuse: false + } } ) { - id - method + uuid + action state } } `, variables: { - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //The recipient of the initial supply - collection_id: 2406, //Specify the collection ID - token_id: 0, //Specify the token ID - initial_supply: 1, //Mint initial supply - cap: "INFINITE" //Define supply type + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", + collectionId: 2406, + tokenId: 0, + initialSupply: 1 } }, { - headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN' } }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -310,45 +321,49 @@ import requests query = ''' mutation CreateToken( $recipient: String! - $collection_id: BigInt! - $token_id: BigInt - $initial_supply: BigInt - $cap: TokenMintCapType! + $collectionId: BigInt! + $tokenId: BigInt! + $initialSupply: BigInt! ) { - CreateToken( - recipient: $recipient - collectionId: $collection_id - params: { - tokenId: { integer: $token_id } - initialSupply: $initial_supply - cap: { type: $cap } + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + createToken: { + recipient: $recipient + collectionId: $collectionId + tokenId: $tokenId + initialSupply: $initialSupply + listingForbidden: false + infusion: 0 + anyoneCanInfuse: false + } } ) { - id - method + uuid + action state } } ''' variables = { - 'recipient': 'cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f', #The recipient of the initial supply - 'collection_id': 2406, #Specify the collection ID - 'token_id': 0, #Specify the token ID - 'initial_supply': 1, #Mint initial supply - 'cap': 'INFINITE' #Define supply type + 'recipient': 'cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f', + 'collectionId': 2406, + 'tokenId': 0, + 'initialSupply': 1, } response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` -A WebSocket event will also be fired so you can pick up the changes in real-time by listening to the app channel on the WebSocket. +The response includes the transaction's `uuid`, `action` (e.g. `MultiTokens.create_token`), and `state` (`PENDING` → `BROADCAST` → `FINALIZED`). Use `GetTransaction(network, chain, uuid: "")` to poll the current state. :::tip For Token ID management, head to [Best Practices > TokenID Structure](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md) @@ -356,7 +371,8 @@ For Token ID management, head to [Best Practices > TokenID Structure](/02-guides :::info Explore More Arguments For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -For instance, you'll find settings such as adding attributes/royalties/supply type and much more with the `CreateTokenParams` argument, or the ability to sign using a managed wallet with the `signingAccount` argument. + +In addition to the fields shown above, `createToken` accepts `cap`, `behavior` (currency / royalty configuration), `attributes`, and `groups` (token-group membership). To sign with a managed wallet instead of the Wallet Daemon, set `signerAccount` on `CreateTransaction`. ::: :::tip What's next? From 15c9c17bc71b00ea994faf234a323376c5fb30c6 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 13 May 2026 14:14:20 +0300 Subject: [PATCH 11/67] Migrate Creating a Currency Token page to v3 - In v3, a token becomes a currency via behavior: { type: IS_CURRENCY, name, symbol, decimalCount }. Replace the v2 params.metadata shape with the new TokenBehaviorInput on createToken - Wrap the call in CreateTransaction (single v3 mutation envelope) - Flat tokenId BigInt; Bearer auth; v3 required NON_NULL fields populated - Add 'Platform v3 SDKs not yet available' warning above the SDK tabs - Drop the dead WebSocket-event line Co-Authored-By: Claude Opus 4.7 (1M context) --- .../02-creating-a-currency-token.md | 198 ++++++++++-------- 1 file changed, 112 insertions(+), 86 deletions(-) diff --git a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md index 0d864db..30f665a 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md +++ b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md @@ -18,7 +18,7 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa Enjin Blockchain allows you to create customized structures. This flexibility enables you to organize your tokens in various ways that suit your needs. -For the currency token options, configure the metadata parameter, which consists of 3 values: +In v3 a token becomes a currency by setting its `behavior` field to `{ type: IS_CURRENCY, name, symbol, decimalCount }`: - **Name:** The token name (e.g. Gold Coins). - **Symbol:** A short symbol that represents the token (e.g. GOLD). @@ -49,25 +49,39 @@ For the time being, you can create currency token via [Option B. Using the Enjin ## Option B. Using the Enjin API & SDKs -CreateToken mutation enables you to create a new token within an existing collection. This operation is essential for introducing new digital assets, and it allows you to define various attributes and characteristics for the newly created token. -For the currency token options, configure the metadata parameter, which consists of name, symbol and decimals +To create a currency token, use the standard `createToken` action with the `behavior` field set to `{ type: IS_CURRENCY, name, symbol, decimalCount }`. The rest of the input (recipient, collectionId, tokenId, initialSupply, etc.) follows the same shape as a regular [token create](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md#option-b-using-the-enjin-api--sdks). + +:::warning Platform v3 SDKs are not yet available +The C# and C++ SDK examples below still target the Enjin Platform v2 API and **will not work against v3**. This section will be updated once the v3 SDKs ship. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +::: ```graphql -mutation CreateCurrencyToken{ - CreateToken( - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #The recipient of the initial supply - collectionId: 2406 #Set the collection ID - params:{ - tokenId: {integer: 0} #Set the token ID - initialSupply: 1 #Mint initial supply - cap: {type: INFINITE} #Define supply type - metadata: {name: "Gold Coins", symbol: "GOLD", decimalCount: 2} #Define currency configuration here +mutation CreateCurrencyToken { + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + createToken: { + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" # recipient of the initial supply + collectionId: 2406 # collection to mint into + tokenId: 0 # the new token ID + initialSupply: 1 # initial supply to mint + listingForbidden: false + infusion: 0 + anyoneCanInfuse: false + behavior: { # makes this token a currency + type: IS_CURRENCY + name: "Gold Coins" + symbol: "GOLD" + decimalCount: 2 + } + } } ) { - id - method + uuid + action state } } @@ -77,8 +91,8 @@ mutation CreateCurrencyToken{ ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation CreateCurrencyToken(\r\n $recipient: String!\r\n $collection_id: BigInt!\r\n $token_id: BigInt\r\n $initial_supply: BigInt\r\n $cap: TokenMintCapType!\r\n $metadata: TokenMetadataInput\r\n) {\r\n CreateToken(\r\n recipient: $recipient\r\n collectionId: $collection_id\r\n params: {\r\n tokenId: { integer: $token_id }\r\n initialSupply: $initial_supply\r\n cap: { type: $cap }\r\n metadata: $metadata\r\n }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","collection_id":2406,"token_id":0,"initial_supply":1,"cap":"INFINITE","metadata":{"name":"Gold Coins","symbol":"GOLD","decimalCount":2}}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation CreateCurrencyToken($recipient: String!, $collectionId: BigInt!, $tokenId: BigInt!, $initialSupply: BigInt!, $behavior: TokenBehaviorInput!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: {\r\n createToken: {\r\n recipient: $recipient\r\n collectionId: $collectionId\r\n tokenId: $tokenId\r\n initialSupply: $initialSupply\r\n listingForbidden: false\r\n infusion: 0\r\n anyoneCanInfuse: false\r\n behavior: $behavior\r\n }\r\n }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","collectionId":2406,"tokenId":0,"initialSupply":1,"behavior":{"type":"IS_CURRENCY","name":"Gold Coins","symbol":"GOLD","decimalCount":2}}}' ``` @@ -135,41 +149,44 @@ Work in Progress! ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` - mutation CreateCurrencyToken - ( + mutation CreateCurrencyToken( $recipient: String! - $collection_id: BigInt! - $token_id: BigInt - $initial_supply: BigInt - $cap: TokenMintCapType! - $metadata: TokenMetadataInput + $collectionId: BigInt! + $tokenId: BigInt! + $initialSupply: BigInt! + $behavior: TokenBehaviorInput! ) { - CreateToken( - recipient: $recipient - collectionId: $collection_id - params:{ - tokenId: {integer: $token_id} - initialSupply: $initial_supply - cap: {type: $cap} - metadata: $metadata + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + createToken: { + recipient: $recipient + collectionId: $collectionId + tokenId: $tokenId + initialSupply: $initialSupply + listingForbidden: false + infusion: 0 + anyoneCanInfuse: false + behavior: $behavior + } } ) { - id - method + uuid + action state } } `, variables: { - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //The recipient of the initial supply - collection_id: 2406, //Specify the collection ID - token_id: 0, //Specify the token ID - initial_supply: 1, //Mint initial supply - cap: "INFINITE", //Define supply type - metadata: {name: "Gold Coins", symbol: "GOLD", decimalCount: 2} //Define currency configuration here + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", + collectionId: 2406, + tokenId: 0, + initialSupply: 1, + behavior: { type: "IS_CURRENCY", name: "Gold Coins", symbol: "GOLD", decimalCount: 2 } } }), }) @@ -185,38 +202,42 @@ axios.post('https://platform.beta.enjin.io/graphql', { query: ` mutation CreateCurrencyToken( $recipient: String! - $collection_id: BigInt! - $token_id: BigInt - $initial_supply: BigInt - $cap: TokenMintCapType! - $metadata: TokenMetadataInput + $collectionId: BigInt! + $tokenId: BigInt! + $initialSupply: BigInt! + $behavior: TokenBehaviorInput! ) { - CreateToken( - recipient: $recipient - collectionId: $collection_id - params: { - tokenId: { integer: $token_id } - initialSupply: $initial_supply - cap: { type: $cap } - metadata: $metadata + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + createToken: { + recipient: $recipient + collectionId: $collectionId + tokenId: $tokenId + initialSupply: $initialSupply + listingForbidden: false + infusion: 0 + anyoneCanInfuse: false + behavior: $behavior + } } ) { - id - method + uuid + action state } } `, variables: { - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //The recipient of the initial supply - collection_id: 2406, //Specify the collection ID - token_id: 0, //Specify the token ID - initial_supply: 1, //Mint initial supply - cap: "INFINITE", //Define supply type - metadata: {name: "Gold Coins", symbol: "GOLD", decimalCount: 2} //Define currency configuration here + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", + collectionId: 2406, + tokenId: 0, + initialSupply: 1, + behavior: { type: "IS_CURRENCY", name: "Gold Coins", symbol: "GOLD", decimalCount: 2 } } }, { - headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN' } }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -229,48 +250,52 @@ import requests query = ''' mutation CreateCurrencyToken( $recipient: String! - $collection_id: BigInt! - $token_id: BigInt - $initial_supply: BigInt - $cap: TokenMintCapType! - $metadata: TokenMetadataInput + $collectionId: BigInt! + $tokenId: BigInt! + $initialSupply: BigInt! + $behavior: TokenBehaviorInput! ) { - CreateToken( - recipient: $recipient - collectionId: $collection_id - params: { - tokenId: { integer: $token_id } - initialSupply: $initial_supply - cap: { type: $cap } - metadata: $metadata + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + createToken: { + recipient: $recipient + collectionId: $collectionId + tokenId: $tokenId + initialSupply: $initialSupply + listingForbidden: false + infusion: 0 + anyoneCanInfuse: false + behavior: $behavior + } } ) { - id - method + uuid + action state } } ''' variables = { - 'recipient': 'cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f', #The recipient of the initial supply - 'collection_id': 2406, #Specify the collection ID - 'token_id': 0, #Specify the token ID - 'initial_supply': 1, #Mint initial supply - 'cap': 'INFINITE', #Define supply type - 'metadata': {'name': "Gold Coins", 'symbol': "GOLD", 'decimalCount': 2} #Define currency configuration here + 'recipient': 'cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f', + 'collectionId': 2406, + 'tokenId': 0, + 'initialSupply': 1, + 'behavior': {'type': 'IS_CURRENCY', 'name': 'Gold Coins', 'symbol': 'GOLD', 'decimalCount': 2}, } response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` -A WebSocket event will also be fired so you can pick up the changes in real-time by listening to the app channel on the WebSocket. +The response includes the transaction's `uuid`, `action` (e.g. `MultiTokens.create_token`), and `state` (`PENDING` → `BROADCAST` → `FINALIZED`). Use `GetTransaction(network, chain, uuid: "")` to poll the current state. :::tip For Token ID management, head to [Best Practices > TokenID Structure](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md) @@ -278,7 +303,8 @@ For Token ID management, head to [Best Practices > TokenID Structure](/02-guides :::info Explore More Arguments For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -For instance, you'll find settings such as adding attributes/royalties/supply type and much more with the `CreateTokenParams` argument, or the ability to sign using a managed wallet with the `signingAccount` argument. + +`createToken` also accepts `cap`, `attributes`, and `groups` (token-group membership). To sign with a managed wallet instead of the Wallet Daemon, set `signerAccount` on `CreateTransaction`. ::: :::tip What's next? From 65339ccb7b35f42a1133031facab8734157e87d8 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 13 May 2026 14:16:13 +0300 Subject: [PATCH 12/67] Migrate Adding Metadata page to v3 - Replace BatchSetAttribute (v2 omit-tokenId-for-collection shape) with the two discrete v3 discriminator actions on CreateTransaction: batchSetTokenAttribute and batchSetCollectionAttribute - Flat tokenId BigInt; Bearer auth; updated cross-callout about the collection variant of the action - Add 'Platform v3 SDKs not yet available' warning above SDK tabs - Drop v2 gifs and screenshots, plus the dead WebSocket-event line - Mention setTokenAttribute / removeTokenAttribute / removeAllTokenAttributes (and their collection counterparts) as related discriminator fields - Update wallet-signing callout to v3 Signing Account pattern Co-Authored-By: Claude Opus 4.7 (1M context) --- .../01-managing-tokens/03-adding-metadata.md | 201 +++++++++--------- 1 file changed, 103 insertions(+), 98 deletions(-) diff --git a/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md b/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md index db0b751..68e495d 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md +++ b/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md @@ -61,7 +61,7 @@ Check the [Best Practices for Collection/Token Metadata](/02-guides/01-platform/ :::warning Attributes for Collections and Tokens are very similar. The process of adding attributes is similar for both collections and tokens. While this tutorial guides you through adding an attribute to a token, you can follow the same steps to add attributes to a collection. -Simply navigate to the corresponding menu for collections instead of tokens. +Simply navigate to the corresponding menu for collections instead of tokens — and on the API, use the `batchSetCollectionAttribute` action (described below) in place of `batchSetTokenAttribute`. ::: :::warning **Important:** Attribute keys are case sensitive. @@ -76,63 +76,58 @@ Ensure you use the correct casing when defining attributes to avoid errors. ### Option A. Using the Enjin Dashboard In the Platform menu, navigate to "**[Tokens](https://platform.beta.enjin.io/tokens)**". -**Locate the token** you wish to add / edit attributes for, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Attributes**" button. - -![Adding Metadata](/img/guides/managing-tokens/adding-metadata.gif) +**Locate the token** you wish to add / edit attributes for, click the **3 vertical dots** (**⋮**) to its right, then click the "**Attributes**" button. :::tip Need to add multiple attributes for a token? Click on the "**Batch**" button, followed by "**Batch SetAttribute**". ::: -To add / edit an attribute, select the "**Set**" option, type in the "**Key**" that you wish to add / edit, and it's "**Value**" in the corresponding text fields. - -Once you're satisfied with the options, click on the "**Set Attribute**" button at the bottom right corner to create the request. +To add / edit an attribute, select the "**Set**" option, type in the "**Key**" that you wish to add / edit, and its "**Value**" in the corresponding text fields. -

- Set Attribute form -

+Once you're satisfied with the options, click on the "**Set Attribute**" button at the bottom right corner to submit the request. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. -The Transaction Request will then appear in the "**Transactions**" menu. +Since this request requires a , it must be signed before it broadcasts. -

- Set Attribute Banner -

+- By default, transactions are signed automatically by the **Wallet Daemon**. +- To sign with a different account, expand **Transaction Options → Signing Account** on the form and provide a [Managed Wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) address. -![Pending Set Attribute Banner](/img/guides/managing-tokens/pending-set-attr-txn.png) +### Option B. Using the Enjin API & SDKs -Since this request requires a , it'll need to be signed with your Wallet. +In v3, batched attributes are split into two discriminator actions on `CreateTransaction`: -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. +- `batchSetTokenAttribute` — sets multiple attributes on a single token (`collectionId`, `tokenId`, `attributes`). +- `batchSetCollectionAttribute` — sets multiple attributes on a collection (`id`, `attributes`). -### Option B. Using the Enjin API & SDKs +The example below sets three attributes on a token. To target the collection itself, swap `batchSetTokenAttribute` for `batchSetCollectionAttribute` and replace the `collectionId` / `tokenId` pair with a single `id` field equal to the collection ID. -The BatchSetAttribute mutation allows you to efficiently set or update multiple attributes for a specific token within a collection in a single blockchain transaction. Attributes represent various properties, characteristics, or metadata associated with a token. +:::warning Platform v3 SDKs are not yet available +The C# and C++ SDK examples below still target the Enjin Platform v2 API and **will not work against v3**. This section will be updated once the v3 SDKs ship. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +::: ```graphql -mutation BatchSetAttribute { - BatchSetAttribute( - collectionId: 36105 # Specify the collection ID - tokenId: {integer: 0 } #Specify the token ID. If you wish to add collection metadata, omit this line entirely. - attributes: [ - { - key: "name" #Provide an attribute name - value: "Chronicles of the Celestium" #Provide an attribute value - }, - { - key: "description" #Provide an attribute name - value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." #Provide an attribute value - }, - { - key: "uri" #Provide an attribute name - value: "https://yourhost/metadata.json" #Provide an attribute value +mutation BatchSetTokenAttribute { + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + batchSetTokenAttribute: { + collectionId: 36105 + tokenId: 0 + attributes: [ + { key: "name", value: "Chronicles of the Celestium" } + { + key: "description" + value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." + } + { key: "uri", value: "https://yourhost/metadata.json" } + ] } - ] + } ) { - id - method + uuid + action state } } @@ -142,8 +137,8 @@ mutation BatchSetAttribute { ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation BatchSetAttribute($collection_id: BigInt!, $token_id: BigInt) {\r\n BatchSetAttribute(\r\n collectionId: $collection_id\r\n tokenId: { integer: $token_id }\r\n attributes: [\r\n { key: \"name\", value: \"Chronicles of the Celestium\" }\r\n {\r\n key: \"description\"\r\n value: \"An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse.\"\r\n }\r\n { key: \"uri\", value: \"https://yourhost/metadata.json\" }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":0}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation BatchSetTokenAttribute($collectionId: BigInt!, $tokenId: BigInt!, $attributes: [AttributeInput!]!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: {\r\n batchSetTokenAttribute: {\r\n collectionId: $collectionId\r\n tokenId: $tokenId\r\n attributes: $attributes\r\n }\r\n }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"collectionId":36105,"tokenId":0,"attributes":[{"key":"name","value":"Chronicles of the Celestium"},{"key":"description","value":"An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse."},{"key":"uri","value":"https://yourhost/metadata.json"}]}}' ``` @@ -288,37 +283,35 @@ int main() { ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` - mutation BatchSetAttribute($collection_id: BigInt!, $token_id: BigInt) { - BatchSetAttribute( - collectionId: $collection_id - tokenId: { integer: $token_id } - attributes: [ - { - key: "name" - value: "Chronicles of the Celestium" - } - { - key: "description" - value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." - } - { - key: "uri" - value: "https://yourhost/metadata.json" + mutation BatchSetTokenAttribute($collectionId: BigInt!, $tokenId: BigInt!, $attributes: [AttributeInput!]!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + batchSetTokenAttribute: { + collectionId: $collectionId + tokenId: $tokenId + attributes: $attributes } - ] + } ) { - id - method + uuid + action state } } `, variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0 //Specify the token ID. + collectionId: 36105, + tokenId: 0, + attributes: [ + { key: "name", value: "Chronicles of the Celestium" }, + { key: "description", value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." }, + { key: "uri", value: "https://yourhost/metadata.json" } + ] } }), }) @@ -332,31 +325,35 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation BatchSetAttribute($collection_id: BigInt!, $token_id: BigInt) { - BatchSetAttribute( - collectionId: $collection_id - tokenId: { integer: $token_id } - attributes: [ - { key: "name", value: "Chronicles of the Celestium" } - { - key: "description" - value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." + mutation BatchSetTokenAttribute($collectionId: BigInt!, $tokenId: BigInt!, $attributes: [AttributeInput!]!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + batchSetTokenAttribute: { + collectionId: $collectionId + tokenId: $tokenId + attributes: $attributes } - { key: "uri", value: "https://yourhost/metadata.json" } - ] + } ) { - id - method + uuid + action state } } `, variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0 //Specify the token ID. + collectionId: 36105, + tokenId: 0, + attributes: [ + { key: "name", value: "Chronicles of the Celestium" }, + { key: "description", value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." }, + { key: "uri", value: "https://yourhost/metadata.json" } + ] } }, { - headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN' } }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -367,41 +364,49 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation BatchSetAttribute($collection_id: BigInt!, $token_id: BigInt) { - BatchSetAttribute( - collectionId: $collection_id - tokenId: { integer: $token_id } - attributes: [ - { key: "name", value: "Chronicles of the Celestium" } - { - key: "description" - value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." +mutation BatchSetTokenAttribute($collectionId: BigInt!, $tokenId: BigInt!, $attributes: [AttributeInput!]!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + batchSetTokenAttribute: { + collectionId: $collectionId + tokenId: $tokenId + attributes: $attributes } - { key: "uri", value: "https://yourhost/metadata.json" } - ] + } ) { - id - method + uuid + action state } } ''' variables = { - 'collection_id': 36105, #Specify the collection ID - 'token_id': 0, #Specify the token ID. + 'collectionId': 36105, + 'tokenId': 0, + 'attributes': [ + {'key': 'name', 'value': 'Chronicles of the Celestium'}, + {'key': 'description', 'value': 'An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse.'}, + {'key': 'uri', 'value': 'https://yourhost/metadata.json'}, + ], } response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` -A WebSocket event will also be fired so you can pick up the changes in real-time by listening to the app channel on the WebSocket. +The response includes the transaction's `uuid`, `action` (e.g. `MultiTokens.batch_set_attribute`), and `state` (`PENDING` → `BROADCAST` → `FINALIZED`). Use `GetTransaction(network, chain, uuid: "")` to poll the current state. + +:::tip Setting a single attribute, or removing one +Use `setTokenAttribute` / `setCollectionAttribute` to write a single key, `removeTokenAttribute` / `removeCollectionAttribute` to remove one, and `removeAllTokenAttributes` / `removeAllCollectionAttributes` to wipe everything. All of these are discriminator fields on `CreateTransaction.transaction`. +::: ## JSON File: Off-Chain & IPFS Metadata @@ -448,9 +453,9 @@ Check the [Best Practices for Collection/Token Metadata](/02-guides/01-platform/ :::info Explore More Arguments For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -For instance, you'll find settings such as `continueOnFailure` to skip data that would cause the whole batch to fail, or the ability to sign using a managed wallet with the `signingAccount` argument. +To sign with a managed wallet instead of the Wallet Daemon, set `signerAccount` on `CreateTransaction`. ::: :::tip What's next? To mint some token supply, head to the [Minting Tokens](/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md) tutorial. -::: \ No newline at end of file +::: From 79719d08227ec5ffef365d89c7bfc004e70de35f Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 13 May 2026 14:17:45 +0300 Subject: [PATCH 13/67] Migrate Minting Tokens page to v3 - Replace BatchMint (recipients: [{account, mintParams: {tokenId: {integer}, amount}}]) with the v3 mintTokens discriminator action: a flat {collectionId, tokens: [{recipient, tokenId, amount}]} input on CreateTransaction - Mention single-recipient mintToken alternative for completeness - Flat tokenId BigInt; Bearer auth - Add 'Platform v3 SDKs not yet available' warning above SDK tabs - Drop v2 gif and screenshots, plus the dead WebSocket-event line - Update wallet-signing callout to v3 Signing Account pattern Co-Authored-By: Claude Opus 4.7 (1M context) --- .../01-managing-tokens/04-minting-a-token.md | 182 +++++++++--------- 1 file changed, 96 insertions(+), 86 deletions(-) diff --git a/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md b/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md index e2326c8..44e76e5 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md +++ b/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md @@ -30,51 +30,52 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa ## Option A. Using the Enjin Dashboard In the Platform menu, navigate to "**[Tokens](https://platform.beta.enjin.io/tokens)**". -**Locate the token** you wish to mint, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Mint**" button. +**Locate the token** you wish to mint, click the **3 vertical dots** (**⋮**) to its right, then click the "**Mint**" button. -![Minting a Token](/img/guides/managing-tokens/minting-a-token.gif) +Set the recipient and the amount in the corresponding fields, and click on "**Mint**". -Set the recipient and the amount in the corresponding fields, and Click on "**Mint**" +The Transaction Request will then appear in the "**Transactions**" menu. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. -

- Mint Token Form -

+Since this request requires a , it must be signed before it broadcasts. -The Transaction Request will then appear in the "**Transactions**" menu. +- By default, transactions are signed automatically by the **Wallet Daemon**. +- To sign with a different account, expand **Transaction Options → Signing Account** on the form and provide a [Managed Wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) address. -

- Mint Transaction Request Banner -

- -![Pending Mint Transaction](/img/guides/managing-tokens/pending-mint-txn.png) +## Option B. Using the Enjin API & SDKs -Since this request requires a , it'll need to be signed with your Wallet. +In v3, minting is split into two discriminator actions on `CreateTransaction`: -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. +- `mintToken` — mints to a single recipient (`recipient`, `collectionId`, `tokenId`, `amount`, optional `unitPrice`). +- `mintTokens` — mints to multiple recipients in one transaction (`collectionId` + a `tokens: [MintTokenEntryInput!]` list where each entry has `recipient`, `tokenId`, `amount`, optional `unitPrice`). -## Option B. Using the Enjin API & SDKs +The example below uses `mintTokens` so it scales naturally if you add more recipients to the array. -The BatchMint mutation enables you to efficiently create multiple tokens within a single blockchain transaction. This process, known as batch minting, simplifies the minting of multiple tokens, reducing transaction fees and processing time. +:::warning Platform v3 SDKs are not yet available +The C# and C++ SDK examples below still target the Enjin Platform v2 API and **will not work against v3**. This section will be updated once the v3 SDKs ship. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +::: ```graphql -mutation BatchMint { - BatchMint( - collectionId: "7154" #Specify the collection ID - recipients: [ - { - account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" #The recipient of the mint - mintParams: { - amount:1 #Amount to mint - tokenId: {integer: 6533} #Token ID to mint - } +mutation MintTokens { + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + mintTokens: { + collectionId: 7154 + tokens: [ + { + recipient: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" + tokenId: 6533 + amount: 1 + } + ] } - ] + } ) { - id - method + uuid + action state } } @@ -84,8 +85,8 @@ mutation BatchMint { ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation BatchMint($collection_id: BigInt!) {\r\n BatchMint(\r\n collectionId: $collection_id\r\n recipients: [\r\n {\r\n account: \"0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921\"\r\n mintParams: {\r\n amount: 1\r\n tokenId: { integer: 6533 }\r\n }\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"collection_id":7154}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation MintTokens($collectionId: BigInt!, $tokens: [MintTokenEntryInput!]!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: {\r\n mintTokens: {\r\n collectionId: $collectionId\r\n tokens: $tokens\r\n }\r\n }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"collectionId":7154,"tokens":[{"recipient":"0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921","tokenId":6533,"amount":1}]}}' ``` @@ -216,32 +217,35 @@ int main() { ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` - mutation BatchMint( - $collection_id: BigInt! - ) { - BatchMint( - collectionId: $collection_id - recipients: [ - { - account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" #The recipient of the mint - mintParams: { - amount:1 #Amount to mint - tokenId: {integer: 6533} #Token ID to mint - } + mutation MintTokens($collectionId: BigInt!, $tokens: [MintTokenEntryInput!]!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + mintTokens: { + collectionId: $collectionId + tokens: $tokens } - ] + } ) { - id - method + uuid + action state } } `, variables: { - collection_id: 7154 //Specify the collection ID + collectionId: 7154, + tokens: [ + { + recipient: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921", + tokenId: 6533, + amount: 1 + } + ] } }), }) @@ -255,32 +259,35 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation BatchMint( - $collection_id: BigInt! - ) { - BatchMint( - collectionId: $collection_id - recipients: [ - { - account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" #The recipient of the mint - mintParams: { - amount:1 #Amount to mint - tokenId: {integer: 6533} #Token ID to mint - } + mutation MintTokens($collectionId: BigInt!, $tokens: [MintTokenEntryInput!]!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + mintTokens: { + collectionId: $collectionId + tokens: $tokens } - ] + } ) { - id - method + uuid + action state } } `, variables: { - collection_id: 7154 //Specify the collection ID + collectionId: 7154, + tokens: [ + { + recipient: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921", + tokenId: 6533, + amount: 1 + } + ] } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -291,49 +298,52 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation BatchMint( - $collection_id: BigInt! -) { - BatchMint( - collectionId: $collection_id - recipients: [ - { - account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" #The recipient of the mint - mintParams: { - amount:1 #Amount to mint - tokenId: {integer: 6533} #Token ID to mint - } +mutation MintTokens($collectionId: BigInt!, $tokens: [MintTokenEntryInput!]!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + mintTokens: { + collectionId: $collectionId + tokens: $tokens } - ] + } ) { - id - method + uuid + action state } } ''' variables = { - 'collection_id': 7154 #Specify the collection ID + 'collectionId': 7154, + 'tokens': [ + { + 'recipient': '0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921', + 'tokenId': 6533, + 'amount': 1, + }, + ], } response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` -A WebSocket event will also be fired so you can pick up the transfer transaction in real time by listening to the app channel on the WebSocket. +The response includes the transaction's `uuid`, `action` (e.g. `MultiTokens.batch_mint`), and `state` (`PENDING` → `BROADCAST` → `FINALIZED`). Use `GetTransaction(network, chain, uuid: "")` to poll the current state. :::info Explore More Arguments For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -For instance, you'll find settings such as the ability to sign using a managed wallet with the `signingAccount` argument, or batch create tokens instead of minting existing ones with the `createParams` argument. +If you want to create _and_ mint new tokens together, use `createTokens` instead of `mintTokens`. To sign with a managed wallet instead of the Wallet Daemon, set `signerAccount` on `CreateTransaction`. ::: :::tip You've minted a token! What if you need to transfer a token? proceed to [Transferring Tokens](/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md). -::: \ No newline at end of file +::: From a8a5263dcdb176be19d8cea6af96aa03f01edbc7 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 13 May 2026 14:20:06 +0300 Subject: [PATCH 14/67] Migrate Transferring Tokens page to v3 - Convert SimpleTransferToken to v3 transferToken discriminator (flat recipient/collectionId/tokenId/amount, no params wrapper) - Convert TransferKeepAlive to v3 transferEnj discriminator. v3 has no keepAlive flag at the input level; replace the v2 'Using keepAlive' bullet with a note about that and link the existential-deposit article - Convert BatchTransferBalance to v3 CreateBatchTransaction with multiple transferEnj entries (v3 has no batchTransferEnj discriminator) - Mention batchTransfer (token-level batched transfers within one collection) as the v3 path for that scenario - Flat tokenId BigInt; Bearer auth - Drop the v2 'Distributing Tokens via QR' next-step (Beam removed in v3) - Add 'Platform v3 SDKs not yet available' warning above the SDK tabs - Drop v2 gifs/screenshots and the dead WebSocket-event paragraph - Update wallet-signing callout to v3 Signing Account pattern Co-Authored-By: Claude Opus 4.7 (1M context) --- .../05-transferring-tokens.md | 445 +++++++++--------- 1 file changed, 210 insertions(+), 235 deletions(-) diff --git a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md index aa77da8..36d5d55 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md @@ -29,56 +29,54 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa ## Option A. Using the Enjin Dashboard In the Platform menu, navigate to "**[Tokens](https://platform.beta.enjin.io/tokens)**". -**Locate the token** you wish to transfer, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Transfer**" button. - -![Transferring Token](/img/guides/managing-tokens/transferring-token.gif) +**Locate the token** you wish to transfer, click the **3 vertical dots** (**⋮**) to its right, then click the "**Transfer**" button. :::info Need to perform multiple transfers? Click on the "**Batch**" button, followed by "**Batch Transfer**". ::: Fill in the recipient, amount, and other optional arguments in the corresponding fields. -Once you're satisfied with the options, click on the "**Transfer**" button at the bottom right corner to create the request. - -

- Transfer Token Form -

+Once you're satisfied with the options, click on the "**Transfer**" button at the bottom right corner to submit the request. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. -

- Transfer Token Transaction Banner -

+Since this request requires a , it must be signed before it broadcasts. -![Pending Transfer Transaction](/img/guides/managing-tokens/pending-transfer-txn.png) -Clicking "**View**" on the notification will take you to your Transactions List. +- By default, transactions are signed automatically by the **Wallet Daemon**. +- To sign with a different account, expand **Transaction Options → Signing Account** on the form and provide a [Managed Wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) address. -Since this request requires a , it'll need to be signed with your Wallet. +## Option B. Using the Enjin API & SDKs -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. +In v3, transfers are split into three discriminator actions on `CreateTransaction`: -If you're looking to distribute tokens to your community or players, but don't have their account addresses, don't worry! Our solution is Enjin Beam. -Proceed to the [Distributing Tokens via QR](/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md) tutorial to learn more. +- `transferToken` — transfer a specific token between accounts. +- `transferEnj` — transfer ENJ (or cENJ on Canary) between accounts. +- `batchTransfer` — transfer multiple tokens from one collection to multiple recipients in a single transaction. -## Option B. Using the Enjin API & SDKs +:::warning Platform v3 SDKs are not yet available +The C# and C++ SDK examples below still target the Enjin Platform v2 API and **will not work against v3**. This section will be updated once the v3 SDKs ship. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +::: ### Transferring an asset -`SimpleTransferToken` mutation simplifies the process of transferring a specific token from one wallet to another. It is a straightforward way to facilitate token transfers without the need for complex intermediary steps. +Use the `transferToken` action to move a specific token from the signing account to another wallet. ```graphql -mutation TransferToken{ - SimpleTransferToken( - collectionId: 36105 #Specify the collection ID - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the recipent address - params: { - tokenId: {integer: 0} #Specify the token ID - amount: 1 #Choose the transfer amount +mutation TransferToken { + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + transferToken: { + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" + collectionId: 36105 + tokenId: 0 + amount: 1 + } } - ){ - id - method + ) { + uuid + action state } } @@ -88,8 +86,8 @@ mutation TransferToken{ ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation TransferToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $recipient: String!\r\n $amount: BigInt!\r\n) {\r\n SimpleTransferToken(\r\n collectionId: $collection_id\r\n recipient: $recipient\r\n params: { tokenId: { integer: $token_id }, amount: $amount }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"collection_id":36105,"token_id":0,"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","amount":1}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation TransferToken($recipient: String!, $collectionId: BigInt!, $tokenId: BigInt!, $amount: BigInt!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: {\r\n transferToken: {\r\n recipient: $recipient\r\n collectionId: $collectionId\r\n tokenId: $tokenId\r\n amount: $amount\r\n }\r\n }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","collectionId":36105,"tokenId":0,"amount":1}}' ``` @@ -210,34 +208,33 @@ int main() { ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` - mutation TransferToken( - $collection_id: BigInt! - $token_id: BigInt! - $recipient: String! - $amount: BigInt! - ) { - SimpleTransferToken( - collectionId: $collection_id - recipient: $recipient - params: { - tokenId: {integer: $token_id} - amount: $amount + mutation TransferToken($recipient: String!, $collectionId: BigInt!, $tokenId: BigInt!, $amount: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + transferToken: { + recipient: $recipient + collectionId: $collectionId + tokenId: $tokenId + amount: $amount + } } - ){ - id - method + ) { + uuid + action state } } `, variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0, //Specify the token ID - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address - amount: 1 //Choose the transfer amount + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", + collectionId: 36105, + tokenId: 0, + amount: 1 } }), }) @@ -251,34 +248,33 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation TransferToken( - $collection_id: BigInt! - $token_id: BigInt! - $recipient: String! - $amount: BigInt! - ) { - SimpleTransferToken( - collectionId: $collection_id - recipient: $recipient - params: { - tokenId: {integer: $token_id} - amount: $amount + mutation TransferToken($recipient: String!, $collectionId: BigInt!, $tokenId: BigInt!, $amount: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + transferToken: { + recipient: $recipient + collectionId: $collectionId + tokenId: $tokenId + amount: $amount + } } - ){ - id - method + ) { + uuid + action state } } `, variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0, //Specify the token ID - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address - amount: 1 //Choose the transfer amount + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", + collectionId: 36105, + tokenId: 0, + amount: 1 } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -289,37 +285,36 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation TransferToken( - $collection_id: BigInt! - $token_id: BigInt! - $recipient: String! - $amount: BigInt! -) { - SimpleTransferToken( - collectionId: $collection_id - recipient: $recipient - params: { - tokenId: {integer: $token_id} - amount: $amount +mutation TransferToken($recipient: String!, $collectionId: BigInt!, $tokenId: BigInt!, $amount: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + transferToken: { + recipient: $recipient + collectionId: $collectionId + tokenId: $tokenId + amount: $amount + } } - ){ - id - method + ) { + uuid + action state } } ''' variables = { - 'collection_id': 36105, #Specify the collection ID - 'token_id': 0, #Specify the token ID - 'recipient': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", #Specify the recipent address - 'amount': 1 #Choose the transfer amount + 'recipient': 'cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f', + 'collectionId': 36105, + 'tokenId': 0, + 'amount': 1, } response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` @@ -328,18 +323,24 @@ print(response.json()) ### Transferring ENJ token -To send ENJ / CENJ tokens from one wallet to another, use the `TransferAllowDeath` mutation, or the `TransferKeepAlive` if you want to make sure the account doesn't get reaped ([Read more about account reaping](https://support.enjin.io/hc/en-gb/articles/16297132519569-What-is-the-Existential-Deposit)): +To send ENJ (or cENJ on the Canary network) from one wallet to another, use the `transferEnj` action. ```graphql -mutation TransferENJTokens { - TransferKeepAlive( - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the recipent address - amount: 5000000000000000000 #Specify the amount of tokens to transfer +mutation TransferEnj { + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + transferEnj: { + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" + amount: 5000000000000000000 # 5 ENJ in base units (5 * 10^18) + } + } ) { - id - method + uuid + action state } } @@ -349,8 +350,8 @@ mutation TransferENJTokens { ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation TransferENJTokens(\r\n $recipient: String!\r\n $amount: BigInt!\r\n $keep_alive: Boolean\r\n) {\r\n TransferBalance(\r\n recipient: $recipient\r\n amount: $amount\r\n keepAlive: $keep_alive\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","amount":1,"keep_alive":true}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation TransferEnj($recipient: String!, $amount: BigInt!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: {\r\n transferEnj: {\r\n recipient: $recipient\r\n amount: $amount\r\n }\r\n }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","amount":"5000000000000000000"}}' ``` @@ -393,26 +394,29 @@ Work In Progress ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` - mutation TransferKeepAlive( - $recipient: String! - $amount: BigInt! - ) { - TransferBalance( - recipient: $recipient - amount: $amount - ){ - id - method + mutation TransferEnj($recipient: String!, $amount: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + transferEnj: { + recipient: $recipient + amount: $amount + } + } + ) { + uuid + action state } } `, variables: { - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address - amount: 1 //Specify the amount of tokens to transfer + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", + amount: "5000000000000000000" } }), }) @@ -426,26 +430,29 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation TransferKeepAlive( - $recipient: String! - $amount: BigInt! - ) { - TransferBalance( - recipient: $recipient - amount: $amount - ){ - id - method - state + mutation TransferEnj($recipient: String!, $amount: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + transferEnj: { + recipient: $recipient + amount: $amount + } } + ) { + uuid + action + state } + } `, variables: { - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address - amount: 1 //Specify the amount of tokens to transfer + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", + amount: "5000000000000000000" } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -456,73 +463,64 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation TransferKeepAlive( - $recipient: String! - $amount: BigInt! -) { - TransferBalance( - recipient: $recipient - amount: $amount - ){ - id - method +mutation TransferEnj($recipient: String!, $amount: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + transferEnj: { + recipient: $recipient + amount: $amount + } + } + ) { + uuid + action state } } ''' variables = { - 'recipient': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", #Specify the recipent address - 'amount': 1 #Specify the amount of tokens to transfer - + 'recipient': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", + 'amount': "5000000000000000000", } response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` -:::info **Key Arguments** +:::info Key arguments - **Calculating the `amount`** - The Platform accepts ENJ values in the **base unit** (integers), not decimal ENJ amounts. To calculate the correct input, **multiply your desired ENJ amount by 10^18** (1 quintillion). - **Formula:** `Desired ENJ` \* `1,000,000,000,000,000,000` - **Example:** To transfer **5 ENJ**, input `5000000000000000000`. -- **Using `keepAlive`** - - Set to `true` to prevent the account from being removed (reaped) if the balance drops below the minimum requirement. - - [Learn more about the keepAlive argument here](/03-api-reference/04-important-arguments.md#keepalive) +- **`keepAlive` is no longer an input** — v3's `transferEnj` doesn't expose the `keepAlive` flag that v2's `TransferBalance` / `TransferKeepAlive` did. If you need to send your full balance, send slightly less than the full balance to avoid reaping the source account. [Read more about account reaping](https://support.enjin.io/hc/en-gb/articles/16297132519569-What-is-the-Existential-Deposit). ::: ### Batch Transferring ENJ token -To send ENJ / CENJ tokens from one wallet to multiple addresses or in multiple transactions, we use the `BatchTransferBalance` mutation +To send ENJ to multiple addresses in a single on-chain transaction, use `CreateBatchTransaction` with one `transferEnj` entry per recipient. ```graphql -mutation BatchSendENJ{ - BatchTransferBalance( - recipients: [ - { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", #Specify the recipent address - transferBalanceParams: { - value: 5000000000000000000 #Specify the amount of tokens to transfer - } - }, - { - account: "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", #Specify the recipent address - transferBalanceParams: { - value: 15250000000000000000 #Specify the amount of tokens to transfer - keepAlive: true #Set to true if you want to make sure the account doesn't get reaped - } - }, +mutation BatchTransferEnj { + CreateBatchTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transactions: [ + { transferEnj: { recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", amount: 5000000000000000000 } } + { transferEnj: { recipient: "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", amount: 15250000000000000000 } } ] - ){ - id - transactionId + ) { + uuid + action state } } @@ -532,8 +530,8 @@ mutation BatchSendENJ{ ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation BatchSendENJ($recipients: [TransferRecipient!]!) {\r\n BatchTransferBalance(recipients: $recipients) {\r\n id\r\n transactionId\r\n state\r\n }\r\n}\r\n","variables":{"recipients":[{"account":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","transferBalanceParams":{"value":5000000000000000000}},{"account":"cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu","transferBalanceParams":{"value":15250000000000000000,"keepAlive":true}}]}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation BatchTransferEnj($transactions: [TransactionInput!]!) {\r\n CreateBatchTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transactions: $transactions\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"transactions":[{"transferEnj":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","amount":"5000000000000000000"}},{"transferEnj":{"recipient":"cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu","amount":"15250000000000000000"}}]}}' ``` @@ -593,32 +591,25 @@ Work In Progress ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` - mutation BatchSendENJ($recipients: [TransferRecipient!]!){ - BatchTransferBalance( - recipients: $recipients - ){ - id - transactionId + mutation BatchTransferEnj($transactions: [TransactionInput!]!) { + CreateBatchTransaction( + network: ENJIN + chain: MATRIX + transactions: $transactions + ) { + uuid + action state } } `, variables: { - recipients:[ - { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address - transferBalanceParams: { value: 5000000000000000000 } //Specify the amount of tokens to transfer - }, - { - account: "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", //Specify the recipent address - transferBalanceParams: { - value: 15250000000000000000, //Specify the amount of tokens to transfer - keepAlive: true //Set to true if you want to make sure the account doesn't get reaped - } - } + transactions: [ + { transferEnj: { recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", amount: "5000000000000000000" } }, + { transferEnj: { recipient: "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", amount: "15250000000000000000" } } ] } }), @@ -633,33 +624,26 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation BatchSendENJ($recipients: [TransferRecipient!]!){ - BatchTransferBalance( - recipients: $recipients - ){ - id - transactionId - state - } + mutation BatchTransferEnj($transactions: [TransactionInput!]!) { + CreateBatchTransaction( + network: ENJIN + chain: MATRIX + transactions: $transactions + ) { + uuid + action + state } + } `, variables: { - recipients:[ - { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address - transferBalanceParams: { value: 5000000000000000000 } //Specify the amount of tokens to transfer - }, - { - account: "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", //Specify the recipent address - transferBalanceParams: { - value: 15250000000000000000, //Specify the amount of tokens to transfer - keepAlive: true //Set to true if you want to make sure the account doesn't get reaped - } - } + transactions: [ + { transferEnj: { recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", amount: "5000000000000000000" } }, + { transferEnj: { recipient: "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", amount: "15250000000000000000" } } ] } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -670,43 +654,38 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation BatchSendENJ($recipients: [TransferRecipient!]!){ - BatchTransferBalance( - recipients: $recipients - ){ - id - transactionId +mutation BatchTransferEnj($transactions: [TransactionInput!]!) { + CreateBatchTransaction( + network: ENJIN + chain: MATRIX + transactions: $transactions + ) { + uuid + action state } } ''' variables = { - 'recipients':[ - { - 'account': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", #Specify the recipent address - 'transferBalanceParams': { 'value': 5000000000000000000 } #Specify the amount of tokens to transfer - }, - { - 'account': "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", #Specify the recipent address - 'transferBalanceParams': { - 'value': 15250000000000000000, #Specify the amount of tokens to transfer - 'keepAlive': True #Set to true if you want to make sure the account doesn't get reaped - } - } - ] + 'transactions': [ + {'transferEnj': {'recipient': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", 'amount': "5000000000000000000"}}, + {'transferEnj': {'recipient': "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", 'amount': "15250000000000000000"}}, + ], } response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` -A WebSocket event will also be fired so you can pick up the transfer transaction in real time by listening to the app channel on the WebSocket. +:::tip Batched token transfers +To transfer multiple tokens **from a single collection** to multiple recipients in one transaction, use the `batchTransfer` discriminator action: `{ collectionId, recipients: [{ account, tokenId, amount }] }`. +::: :::tip Need to send a transaction request to user's wallet? This can be done using Enjin Platform API & WalletConnect! @@ -715,10 +694,6 @@ To learn more, check out the [Using WalletConnect page](/02-guides/01-platform/0 :::info Explore More Arguments For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -For instance, you'll find settings such as `continueOnFailure` to skip data that would cause the whole batch to fail, or the ability to sign using a managed wallet with the `signingAccount` argument. -::: -:::What's next? -Distribute tokens to your players, even if they don't have wallets! -Proceed to the [Distributing Tokens via QR](/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md) tutorial to learn more. -::: \ No newline at end of file +To sign with a managed wallet instead of the Wallet Daemon, set `signerAccount` on `CreateTransaction` / `CreateBatchTransaction`. +::: From ee3d20e097f162eb0db6abc5add645b8d7844185 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 13 May 2026 14:22:40 +0300 Subject: [PATCH 15/67] Migrate Freezing & Thawing page to v3 - Split unified v2 Freeze/Thaw + freezeType discriminator into v3's four discrete actions on CreateTransaction: freezeCollection: { collectionId } freezeToken: { collectionId, tokenId, state } thawCollection: { collectionId } thawToken: { collectionId, tokenId, state } - State is now NON_NULL on both freezeToken and thawToken; uses the FreezeState enum (NEVER | PERMANENT | TEMPORARY) - Flat tokenId BigInt; Bearer auth - Add 'Platform v3 SDKs not yet available' warning above SDK tabs - Drop v2 gif and screenshots - Update wallet-signing callout to v3 Signing Account pattern Co-Authored-By: Claude Opus 4.7 (1M context) --- .../01-managing-tokens/07-freezing-thawing.md | 472 +++++++----------- 1 file changed, 192 insertions(+), 280 deletions(-) diff --git a/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md b/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md index 93cecac..68921f5 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md +++ b/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md @@ -55,49 +55,49 @@ Simply navigate to the corresponding menu for tokens instead of collections, or ### Freezing an entire collection In the Platform menu, navigate to "**[Collections](https://platform.beta.enjin.io/collections)**". -**Locate the collection** you wish to freeze, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Freeze**" button. - -![Freezing a Collection](/img/guides/managing-tokens/freezing-collection.gif) +**Locate the collection** you wish to freeze, click the **3 vertical dots** (**⋮**) to its right, then click the "**Freeze**" button. Choose the freeze state, and click on the "**Freeze**" button. -

- Freeze Collection Form -

+The Transaction Request will then appear in the "**Transactions**" menu. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. -The Transaction Request will then appear in the "**Transactions**" menu. +Since this request requires a , it must be signed before it broadcasts. -

- Freeze Collection Transaction Request Banner -

+- By default, transactions are signed automatically by the **Wallet Daemon**. +- To sign with a different account, expand **Transaction Options → Signing Account** on the form and provide a [Managed Wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) address. -![Pending Freeze Collection Transaction Request](/img/guides/managing-tokens/pending-freeze-collection-txn.png) +## Option B. Using the Enjin API & SDKs -Since this request requires a , it'll need to be signed with your Wallet. +In v3, freeze and thaw are split into four discriminator actions on `CreateTransaction`: -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. +- `freezeCollection: { collectionId }` +- `freezeToken: { collectionId, tokenId, state }` — where `state` is `PERMANENT`, `TEMPORARY`, or `NEVER`. +- `thawCollection: { collectionId }` +- `thawToken: { collectionId, tokenId, state }` -## Option B. Using the Enjin API & SDKs +The v2 unified `Freeze(... freezeType: COLLECTION | TOKEN)` shape no longer exists. -### Freezing a Collection or Token - -By freezing a collection, all tokens within that collection will be frozen, meaning they cannot be burned or transferred out of the wallet they're currently in. +:::warning Platform v3 SDKs are not yet available +The C# and C++ SDK examples below still target the Enjin Platform v2 API and **will not work against v3**. This section will be updated once the v3 SDKs ship. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +::: ### Freezing an entire collection -Use the `Freeze` mutation and the `freezeType: COLLECTION` argument to freeze a collection. +Use the `freezeCollection` action — freezing a collection freezes every token in it. ```graphql -mutation FreezeCollection{ - Freeze( - collectionId: 36105 #Specify the collection ID - freezeType: COLLECTION #For collection freezing - ){ - id - method +mutation FreezeCollection { + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + freezeCollection: { collectionId: 36105 } + } + ) { + uuid + action state } } @@ -107,8 +107,8 @@ mutation FreezeCollection{ ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation FreezeCollection($collection_id: BigInt!, $freeze_type: FreezeType!) {\r\n Freeze(collectionId: $collection_id, freezeType: $freeze_type) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"freeze_type":"COLLECTION"}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation FreezeCollection($collectionId: BigInt!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: { freezeCollection: { collectionId: $collectionId } }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"collectionId":36105}}' ``` @@ -210,28 +210,22 @@ int main() { ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` - mutation FreezeCollection - ( - $collection_id: BigInt! - $freeze_type: FreezeType! - ) { - Freeze( - collectionId: $collection_id - freezeType: $freeze_type - ){ - id - method + mutation FreezeCollection($collectionId: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { freezeCollection: { collectionId: $collectionId } } + ) { + uuid + action state } } `, - variables: { - collection_id: 36105, //Specify the collection ID - freeze_type: "COLLECTION" //For collection freezing - } + variables: { collectionId: 36105 } }), }) .then(response => response.json()) @@ -244,27 +238,21 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation FreezeCollection - ( - $collection_id: BigInt! - $freeze_type: FreezeType! - ) { - Freeze( - collectionId: $collection_id - freezeType: $freeze_type - ){ - id - method + mutation FreezeCollection($collectionId: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { freezeCollection: { collectionId: $collectionId } } + ) { + uuid + action state } } `, - variables: { - collection_id: 36105, //Specify the collection ID - freeze_type: "COLLECTION" //For collection freezing - } + variables: { collectionId: 36105 } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -275,30 +263,24 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation FreezeCollection -( - $collection_id: BigInt! - $freeze_type: FreezeType! -) { - Freeze( - collectionId: $collection_id - freezeType: $freeze_type - ){ - id - method +mutation FreezeCollection($collectionId: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { freezeCollection: { collectionId: $collectionId } } + ) { + uuid + action state } } ''' -variables = { - 'collection_id': 36105, #Specify the collection ID - 'freeze_type': "COLLECTION" #For collection freezing -} +variables = {'collectionId': 36105} response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` @@ -309,20 +291,25 @@ Once the transaction is executed, all tokens within the specified collection wil ### Freezing a single token -Use the `Freeze` mutation and the `freezeType: TOKEN` argument to freeze a token. +Use the `freezeToken` action. The `state` field selects the freeze state to apply (`TEMPORARY` for a freeze that can be thawed later, `PERMANENT` for a soulbound-style freeze). ```graphql -mutation FreezeToken{ - Freeze( - collectionId: 36105 #Specify the collection ID - tokenId:{integer: 0} #Specify the token ID - freezeType: TOKEN #For token freezing - freezeState: TEMPORARY #Select the freeze state - ){ - id - method +mutation FreezeToken { + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + freezeToken: { + collectionId: 36105 + tokenId: 0 + state: TEMPORARY # or PERMANENT for a soulbound freeze + } + } + ) { + uuid + action state } } @@ -332,8 +319,8 @@ mutation FreezeToken{ ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation FreezeToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $freeze_type: FreezeType!\r\n $freeze_state: FreezeStateType\r\n) {\r\n Freeze(\r\n collectionId: $collection_id\r\n tokenId: { integer: $token_id }\r\n freezeType: $freeze_type\r\n freezeState: $freeze_state\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":0,"freeze_type":"TOKEN","freeze_state":"TEMPORARY"}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation FreezeToken($collectionId: BigInt!, $tokenId: BigInt!, $state: FreezeState!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: { freezeToken: { collectionId: $collectionId, tokenId: $tokenId, state: $state } }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"collectionId":36105,"tokenId":0,"state":"TEMPORARY"}}' ``` @@ -443,35 +430,22 @@ int main() { ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` - mutation FreezeToken - ( - $collection_id: BigInt! - $token_id: BigInt! - $freeze_type: FreezeType! - $freeze_state: FreezeStateType - - ) { - Freeze( - collectionId: $collection_id - tokenId:{integer: $token_id} - freezeType: $freeze_type - freezeState: $freeze_state - ){ - id - method + mutation FreezeToken($collectionId: BigInt!, $tokenId: BigInt!, $state: FreezeState!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { freezeToken: { collectionId: $collectionId, tokenId: $tokenId, state: $state } } + ) { + uuid + action state } } `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0, //Specify the token ID - freeze_type: "TOKEN", //For token freezing - freeze_state: "TEMPORARY" //Select the freeze state - } + variables: { collectionId: 36105, tokenId: 0, state: "TEMPORARY" } }), }) .then(response => response.json()) @@ -484,34 +458,21 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation FreezeToken - ( - $collection_id: BigInt! - $token_id: BigInt! - $freeze_type: FreezeType! - $freeze_state: FreezeStateType - - ) { - Freeze( - collectionId: $collection_id - tokenId:{integer: $token_id} - freezeType: $freeze_type - freezeState: $freeze_state - ){ - id - method + mutation FreezeToken($collectionId: BigInt!, $tokenId: BigInt!, $state: FreezeState!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { freezeToken: { collectionId: $collectionId, tokenId: $tokenId, state: $state } } + ) { + uuid + action state } } `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0, //Specify the token ID - freeze_type: "TOKEN", //For token freezing - freeze_state: "TEMPORARY" //Select the freeze state - } + variables: { collectionId: 36105, tokenId: 0, state: "TEMPORARY" } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -522,37 +483,24 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation FreezeToken -( - $collection_id: BigInt! - $token_id: BigInt! - $freeze_type: FreezeType! - $freeze_state: FreezeStateType - -) { - Freeze( - collectionId: $collection_id - tokenId:{integer: $token_id} - freezeType: $freeze_type - freezeState: $freeze_state - ){ - id - method +mutation FreezeToken($collectionId: BigInt!, $tokenId: BigInt!, $state: FreezeState!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { freezeToken: { collectionId: $collectionId, tokenId: $tokenId, state: $state } } + ) { + uuid + action state } } ''' -variables = { - 'collection_id': 36105, #Specify the collection ID - 'token_id': 0, #Specify the token ID - 'freeze_type': "TOKEN", #For token freezing - 'freeze_state': "TEMPORARY" #Select the freeze state -} +variables = {'collectionId': 36105, 'tokenId': 0, 'state': 'TEMPORARY'} response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` @@ -569,18 +517,21 @@ Thawing a collection means allowing all tokens within that collection to be acti By thawing a collection, all tokens within that collection will be thawed, meaning they can be burned and transferred out of the wallet they're currently in. -Use the `Thaw` mutation and the `freezeType: COLLECTION` argument to thaw a collection. +Use the `thawCollection` action. ```graphql -mutation ThawCollection{ - Thaw( - collectionId: 36105 #Specify the collection ID - freezeType: COLLECTION #For collection thawing - ){ - id - method +mutation ThawCollection { + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + thawCollection: { collectionId: 36105 } + } + ) { + uuid + action state } } @@ -590,8 +541,8 @@ mutation ThawCollection{ ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation ThawCollection($collection_id: BigInt!, $freeze_type: FreezeType!) {\r\n Thaw(collectionId: $collection_id, freezeType: $freeze_type) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"freeze_type":"COLLECTION"}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation ThawCollection($collectionId: BigInt!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: { thawCollection: { collectionId: $collectionId } }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"collectionId":36105}}' ``` @@ -693,28 +644,22 @@ int main() { ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` - mutation ThawCollection - ( - $collection_id: BigInt! - $freeze_type: FreezeType! - ) { - Thaw( - collectionId: $collection_id - freezeType: $freeze_type - ){ - id - method + mutation ThawCollection($collectionId: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { thawCollection: { collectionId: $collectionId } } + ) { + uuid + action state } } `, - variables: { - collection_id: 36105, //Specify the collection ID - freeze_type: "COLLECTION" //For collection thawing - } + variables: { collectionId: 36105 } }), }) .then(response => response.json()) @@ -727,27 +672,21 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation ThawCollection - ( - $collection_id: BigInt! - $freeze_type: FreezeType! - ) { - Thaw( - collectionId: $collection_id - freezeType: $freeze_type - ){ - id - method + mutation ThawCollection($collectionId: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { thawCollection: { collectionId: $collectionId } } + ) { + uuid + action state } } `, - variables: { - collection_id: 36105, //Specify the collection ID - freeze_type: "COLLECTION" //For collection thawing - } + variables: { collectionId: 36105 } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -758,30 +697,24 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation ThawCollection -( - $collection_id: BigInt! - $freeze_type: FreezeType! -) { - Thaw( - collectionId: $collection_id - freezeType: $freeze_type - ){ - id - method +mutation ThawCollection($collectionId: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { thawCollection: { collectionId: $collectionId } } + ) { + uuid + action state } } ''' -variables = { - 'collection_id': 36105, #Specify the collection ID - 'freeze_type': "COLLECTION" #For collection thawing -} +variables = {'collectionId': 36105} response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` @@ -792,19 +725,25 @@ Once the transaction is executed, all tokens within the specified collection wil ### Thawing a single token -Use the `Thaw` mutation and the `freezeType: TOKEN` argument to thaw a token. +Use the `thawToken` action. The `state` field is the post-thaw freeze state for the token (usually `TEMPORARY` when lifting a temporary freeze). ```graphql -mutation ThawToken{ - Thaw( - collectionId: 36105 #Specify the collection ID - tokenId:{integer: 0} #Specify the token ID - freezeType: TOKEN #For token thawing +mutation ThawToken { + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + thawToken: { + collectionId: 36105 + tokenId: 0 + state: TEMPORARY + } + } ) { - id - method + uuid + action state } } @@ -814,8 +753,8 @@ mutation ThawToken{ ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation ThawToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $freeze_type: FreezeType!\r\n) {\r\n Thaw(\r\n collectionId: $collection_id\r\n tokenId: { integer: $token_id }\r\n freezeType: $freeze_type\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":0,"freeze_type":"TOKEN"}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation ThawToken($collectionId: BigInt!, $tokenId: BigInt!, $state: FreezeState!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: { thawToken: { collectionId: $collectionId, tokenId: $tokenId, state: $state } }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"collectionId":36105,"tokenId":0,"state":"TEMPORARY"}}' ``` @@ -924,31 +863,22 @@ int main() { ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` - mutation ThawToken - ( - $collection_id: BigInt! - $token_id: BigInt! - $freeze_type: FreezeType! - ) { - Thaw( - collectionId: $collection_id - tokenId:{integer: $token_id} - freezeType: $freeze_type + mutation ThawToken($collectionId: BigInt!, $tokenId: BigInt!, $state: FreezeState!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { thawToken: { collectionId: $collectionId, tokenId: $tokenId, state: $state } } ) { - id - method + uuid + action state } } `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0, //Specify the token ID - freeze_type: "TOKEN" //For token thawing - } + variables: { collectionId: 36105, tokenId: 0, state: "TEMPORARY" } }), }) .then(response => response.json()) @@ -961,30 +891,21 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation ThawToken - ( - $collection_id: BigInt! - $token_id: BigInt! - $freeze_type: FreezeType! - ) { - Thaw( - collectionId: $collection_id - tokenId:{integer: $token_id} - freezeType: $freeze_type + mutation ThawToken($collectionId: BigInt!, $tokenId: BigInt!, $state: FreezeState!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { thawToken: { collectionId: $collectionId, tokenId: $tokenId, state: $state } } ) { - id - method + uuid + action state } } `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0, //Specify the token ID - freeze_type: "TOKEN" //For token thawing - } + variables: { collectionId: 36105, tokenId: 0, state: "TEMPORARY" } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -995,33 +916,24 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation ThawToken -( - $collection_id: BigInt! - $token_id: BigInt! - $freeze_type: FreezeType! -) { - Thaw( - collectionId: $collection_id - tokenId:{integer: $token_id} - freezeType: $freeze_type +mutation ThawToken($collectionId: BigInt!, $tokenId: BigInt!, $state: FreezeState!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { thawToken: { collectionId: $collectionId, tokenId: $tokenId, state: $state } } ) { - id - method + uuid + action state } } ''' -variables = { - 'collection_id': 36105, #Specify the collection ID - 'token_id': 0, #Specify the token ID - 'freeze_type': "TOKEN" #For token thawing -} +variables = {'collectionId': 36105, 'tokenId': 0, 'state': 'TEMPORARY'} response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` @@ -1033,7 +945,7 @@ Once the transaction is executed, the specified token will be thawed. :::info Explore More Arguments For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -For instance, you'll find settings such as `continueOnFailure` to skip data that would cause the whole batch to fail, or the ability to sign using a managed wallet with the `signingAccount` argument. +To sign with a managed wallet instead of the Wallet Daemon, set `signerAccount` on `CreateTransaction`. ::: :::tip Need to send a transaction request to user's wallet? From eb978dfddf812675744da07a8780a7fb2b0f5858 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 13 May 2026 14:24:37 +0300 Subject: [PATCH 16/67] Migrate Melting/Destroying page (collection-destroy sub-section deferred) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Convert Burn to v3 burnToken discriminator action on CreateTransaction (BurnTokenInput is flat: collectionId, tokenId, amount, removeTokenStorage). removeTokenStorage is NON_NULL in v3 so it's always set - Same action handles 'melt some supply' (removeTokenStorage: false) and 'destroy token entirely' (removeTokenStorage: true) - Update RemoveAllAttributes reference to v3's removeAllTokenAttributes discriminator - Drop v2 gifs/screenshots in the migrated sections and replace the v2 wallet-signing callouts with v3 Signing Account guidance - Add 'Platform v3 SDKs not yet available' warning above the API tabs - 'Destroying a collection' sub-section LEFT UNCHANGED (still v2 wording + v2 mutation) with a 'Pending v3 confirmation' callout — v3 has no destroyCollection discriminator or top-level mutation. Awaiting the Enjin team on whether collection destruction is intentionally removed, replaced by another action, or not yet wired up Co-Authored-By: Claude Opus 4.7 (1M context) --- .../08-burning-destroying-tokens.md | 316 ++++++++---------- 1 file changed, 144 insertions(+), 172 deletions(-) diff --git a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md index 8896e7e..8d2a210 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md @@ -28,28 +28,16 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa ### Melting token's supply In the Platform menu, navigate to "**[Tokens](https://platform.beta.enjin.io/tokens)**". -**Locate the token** you wish to melt, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Burn**" button. - -![melting a Token](/img/guides/managing-tokens/burning-token.gif) +**Locate the token** you wish to melt, click the **3 vertical dots** (**⋮**) to its right, then click the "**Burn**" button. Insert the amount of tokens to melt, and click on the "**Burn**" button. -

- Melt Token Form -

- -The Transaction Request will then appear in the "**Transactions**" menu. - -

- Melt Transaction Request Banner -

+The Transaction Request will then appear in the "**Transactions**" menu. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. -![Pending melt Transaction](/img/guides/managing-tokens/pending-burn-token-txn.png) - -Since this request requires a , it'll need to be signed with your Wallet. +Since this request requires a , it must be signed before it broadcasts. -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. +- By default, transactions are signed automatically by the **Wallet Daemon**. +- To sign with a different account, expand **Transaction Options → Signing Account** on the form and provide a [Managed Wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) address. ### Destroying a token and removing it from the Blockchain @@ -68,12 +56,12 @@ While destroying a token removes the token from the blockchain, and retrieves th To destroy a token, follow the above instructions for Melting a token, but make sure to tick the `Remove Token Storage` box. -

- Destroying a Token -

- ### Destroying a collection +:::warning Pending v3 confirmation +This section is awaiting confirmation from the Enjin team on v3 support — the v3 GraphQL schema currently exposes no `destroyCollection` action. The dashboard wording below describes the v2 flow and **may or may not still be available** in the v3 dashboard. We'll refresh this section once we hear back. +::: + :::info To destroy a collection, these requirements must be met: - The caller is the collection owner - The collection has no attributes @@ -85,22 +73,10 @@ To destroy a token, follow the above instructions for Melting a token, but make In the Platform menu, navigate to "**[Collections](https://platform.beta.enjin.io/collections)**". **Locate the collection** you wish to destroy, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Destroy**" button. -![Destroying a Collection](/img/guides/managing-tokens/destroying-collection.gif) - Then, confirm by clicking the "**Destroy**" button -

- Destroy Collection Form -

- The Transaction Request will then appear in the "**Transactions**" menu -

- Destroy Collection Transaction Request Banner -

- -![Pending Destroy Collection Transaction](/img/guides/managing-tokens/pending-destroy-collection-txn.png) - Since this request requires a , it'll need to be signed with your Wallet. - If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. @@ -108,23 +84,32 @@ Since this request requires a , it'll need to b ## Option B. Using the Enjin API & SDKs -### Melting token's supply +In v3, burning is the `burnToken` discriminator action on `CreateTransaction`. The same action handles both "melt some supply" and "destroy the token entirely" — set `removeTokenStorage: true` to destroy. -Use the `Burn` mutation: +:::warning Platform v3 SDKs are not yet available +The C# and C++ SDK examples below still target the Enjin Platform v2 API and **will not work against v3**. This section will be updated once the v3 SDKs ship. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +::: + +### Melting token's supply ```graphql -mutation BurnToken{ - Burn( - collectionId: 68844 #Specify the Collection ID - params: { - tokenId: {integer: 0} #Specify the Token ID - amount: 1 #Specify the amount of supply to burn +mutation BurnToken { + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + burnToken: { + collectionId: 68844 + tokenId: 0 + amount: 1 + removeTokenStorage: false # set true to also destroy the token (see below) + } } ) { - id - method + uuid + action state } } @@ -134,8 +119,8 @@ mutation BurnToken{ ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation BurnToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $amount: BigInt!\r\n) {\r\n Burn(\r\n collectionId: $collection_id\r\n params: { tokenId: { integer: $token_id }, amount: $amount }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":5,"amount":1}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation BurnToken($collectionId: BigInt!, $tokenId: BigInt!, $amount: BigInt!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: { burnToken: { collectionId: $collectionId, tokenId: $tokenId, amount: $amount, removeTokenStorage: false } }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"collectionId":68844,"tokenId":0,"amount":1}}' ``` @@ -183,32 +168,29 @@ Snippet In Progress ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` - mutation BurnToken( - $collection_id: BigInt! - $token_id: BigInt! - $amount: BigInt! - ){ - Burn( - collectionId: $collection_id - params: { - tokenId: {integer: $token_id} - amount: $amount + mutation BurnToken($collectionId: BigInt!, $tokenId: BigInt!, $amount: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + burnToken: { + collectionId: $collectionId + tokenId: $tokenId + amount: $amount + removeTokenStorage: false + } } ) { - id - method + uuid + action state } } `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 5, //Specify the amount of supply to burn - amount: 1 //Specify the amount of supply to burn - } + variables: { collectionId: 68844, tokenId: 0, amount: 1 } }), }) .then(response => response.json()) @@ -221,31 +203,28 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation BurnToken( - $collection_id: BigInt! - $token_id: BigInt! - $amount: BigInt! - ){ - Burn( - collectionId: $collection_id - params: { - tokenId: {integer: $token_id} - amount: $amount + mutation BurnToken($collectionId: BigInt!, $tokenId: BigInt!, $amount: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + burnToken: { + collectionId: $collectionId + tokenId: $tokenId + amount: $amount + removeTokenStorage: false + } } ) { - id - method + uuid + action state } } `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 5, //Specify the amount of supply to burn - amount: 1 //Specify the amount of supply to burn - } + variables: { collectionId: 68844, tokenId: 0, amount: 1 } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -256,68 +235,69 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation BurnToken( - $collection_id: BigInt! - $token_id: BigInt! - $amount: BigInt! -){ - Burn( - collectionId: $collection_id - params: { - tokenId: {integer: $token_id} - amount: $amount +mutation BurnToken($collectionId: BigInt!, $tokenId: BigInt!, $amount: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + burnToken: { + collectionId: $collectionId + tokenId: $tokenId + amount: $amount + removeTokenStorage: false + } } ) { - id - method + uuid + action state } } ''' -variables = { - 'collection_id': 36105, #Specify the collection ID - 'token_id': 5, #Specify the amount of supply to burn - 'amount': 1 #Specify the amount of supply to burn -} +variables = {'collectionId': 68844, 'tokenId': 0, 'amount': 1} response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` -Once the transaction is executed, the token supply will be burned +Once the transaction is executed, the token supply will be burned. ### Destroying a token and removing it from the Blockchain :::info To destroy a token, these requirements must be met: - The caller is the collection owner - The token has no attributes - - If the token has attributes, they can be removed using the `RemoveAllAttributes` mutation + - If the token has attributes, they can be removed using the `removeAllTokenAttributes` action (see [Adding Metadata](/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md#option-b-using-the-enjin-api--sdks)). - The token has 0 supply - - You can remove the supply and destroy the token in the same Burn transaction. + - You can remove the supply and destroy the token in the same `burnToken` transaction. ::: -Use the `Burn` mutation, and add `removeTokenStorage: true` property +Use the same `burnToken` action and set `removeTokenStorage: true`: ```graphql -mutation DestroyToken{ - Burn( - collectionId: 68844 #Specify the Collection ID - params: { - tokenId: {integer: 0} #Specify the Token ID - amount: 1 #Specify the amount of supply to burn - removeTokenStorage: true +mutation DestroyToken { + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + burnToken: { + collectionId: 68844 + tokenId: 0 + amount: 1 + removeTokenStorage: true + } } ) { - id - method + uuid + action state } } @@ -327,8 +307,8 @@ mutation DestroyToken{ ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation BurnToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $amount: BigInt!\r\n) {\r\n Burn(\r\n collectionId: $collection_id\r\n params: {\r\n tokenId: { integer: $token_id }\r\n amount: $amount\r\n removeTokenStorage: true\r\n }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":5,"amount":1}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation DestroyToken($collectionId: BigInt!, $tokenId: BigInt!, $amount: BigInt!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: { burnToken: { collectionId: $collectionId, tokenId: $tokenId, amount: $amount, removeTokenStorage: true } }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"collectionId":68844,"tokenId":0,"amount":1}}' ``` @@ -377,33 +357,29 @@ Snippet In Progress ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` - mutation BurnToken( - $collection_id: BigInt! - $token_id: BigInt! - $amount: BigInt! - ){ - Burn( - collectionId: $collection_id - params: { - tokenId: {integer: $token_id} - amount: $amount - removeTokenStorage: true + mutation DestroyToken($collectionId: BigInt!, $tokenId: BigInt!, $amount: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + burnToken: { + collectionId: $collectionId + tokenId: $tokenId + amount: $amount + removeTokenStorage: true + } } ) { - id - method + uuid + action state } } `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 5, //Specify the amount of supply to burn - amount: 1 //Specify the amount of supply to burn - } + variables: { collectionId: 68844, tokenId: 0, amount: 1 } }), }) .then(response => response.json()) @@ -416,32 +392,28 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation BurnToken( - $collection_id: BigInt! - $token_id: BigInt! - $amount: BigInt! - ){ - Burn( - collectionId: $collection_id - params: { - tokenId: {integer: $token_id} - amount: $amount - removeTokenStorage: true + mutation DestroyToken($collectionId: BigInt!, $tokenId: BigInt!, $amount: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + burnToken: { + collectionId: $collectionId + tokenId: $tokenId + amount: $amount + removeTokenStorage: true + } } ) { - id - method + uuid + action state } } `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 5, //Specify the amount of supply to burn - amount: 1 //Specify the amount of supply to burn - } + variables: { collectionId: 68844, tokenId: 0, amount: 1 } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -452,35 +424,31 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation BurnToken( - $collection_id: BigInt! - $token_id: BigInt! - $amount: BigInt! -){ - Burn( - collectionId: $collection_id - params: { - tokenId: {integer: $token_id} - amount: $amount - removeTokenStorage: true +mutation DestroyToken($collectionId: BigInt!, $tokenId: BigInt!, $amount: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + burnToken: { + collectionId: $collectionId + tokenId: $tokenId + amount: $amount + removeTokenStorage: true + } } ) { - id - method + uuid + action state } } ''' -variables = { - 'collection_id': 36105, #Specify the collection ID - 'token_id': 5, #Specify the amount of supply to burn - 'amount': 1 #Specify the amount of supply to burn -} +variables = {'collectionId': 68844, 'tokenId': 0, 'amount': 1} response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` @@ -491,6 +459,10 @@ Once the transaction is executed, the token will be destroyed and the -Once the transaction is executed, the collection will be destroyed. \ No newline at end of file +Once the transaction is executed, the collection will be destroyed. From 48f0b2a0840a292c5592e81370e515ff31a96f4a Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 13 May 2026 14:35:43 +0300 Subject: [PATCH 17/67] Drop button-position cues across managing-tokens v3 pages The v3 dashboard layout shifted, so 'click the X button at the bottom right corner' no longer reflects where the action button actually lives. Reduce these instructions to just the action ('click the X button') across all four pages that carried the phrasing. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../01-platform/01-managing-tokens/01-creating-collections.md | 2 +- .../01-managing-tokens/02-creating-tokens/02-creating-tokens.md | 2 +- .../01-platform/01-managing-tokens/03-adding-metadata.md | 2 +- .../01-platform/01-managing-tokens/05-transferring-tokens.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md b/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md index 2b28e53..d4c91ed 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md +++ b/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md @@ -37,7 +37,7 @@ From here, you can configure the collection's supply rules, royalties, accepted For a comprehensive view and detail of all available arguments please refer to our [API Reference](/03-api-reference/03-api-reference.md). ::: -Once you're satisfied with the options, click on the "**Create**" button at the bottom right corner to submit the request. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. +Once you're satisfied with the options, click the "**Create**" button to submit the request. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. Since this request requires a , it must be signed before it broadcasts. diff --git a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md index a5e7e0b..31eb46d 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md @@ -50,7 +50,7 @@ From here, you can configure the token's basic settings, supply cap, royalty beh For a comprehensive view and detail of all available arguments please refer to our [API Reference](/03-api-reference/03-api-reference.md). ::: -Once you're satisfied with the options, click on the "**Create**" button at the bottom right corner to submit the request. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. +Once you're satisfied with the options, click the "**Create**" button to submit the request. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. Since this request requires a , it must be signed before it broadcasts. diff --git a/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md b/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md index 68e495d..d348127 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md +++ b/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md @@ -84,7 +84,7 @@ Click on the "**Batch**" button, followed by "**Batch SetAttribute**". To add / edit an attribute, select the "**Set**" option, type in the "**Key**" that you wish to add / edit, and its "**Value**" in the corresponding text fields. -Once you're satisfied with the options, click on the "**Set Attribute**" button at the bottom right corner to submit the request. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. +Once you're satisfied with the options, click the "**Set Attribute**" button to submit the request. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. Since this request requires a , it must be signed before it broadcasts. diff --git a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md index 36d5d55..4a59fce 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md @@ -36,7 +36,7 @@ Click on the "**Batch**" button, followed by "**Batch Transfer**". ::: Fill in the recipient, amount, and other optional arguments in the corresponding fields. -Once you're satisfied with the options, click on the "**Transfer**" button at the bottom right corner to submit the request. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. +Once you're satisfied with the options, click the "**Transfer**" button to submit the request. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. Since this request requires a , it must be signed before it broadcasts. From 7ca35725a38bd3cca0c2c806e626f8f45fa706a0 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 13 May 2026 14:44:45 +0300 Subject: [PATCH 18/67] Drop v2/v3 version framing from managing-tokens guide bodies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These pages are intended to read as canonical Enjin Platform docs once the v3-beta label drops. A reader landing on them shouldn't need to know there was an earlier version, so describe the current API neutrally and remove version comparisons from page bodies and callouts. Changes across all 8 migrated guides: - 'In v3, X is split into …' / 'In v3 a token becomes a currency by …' → describe the current shape directly, no version prefix - 'v3 represents royalties via behavior' / 'v3 supports two cap modes' → 'Royalties are expressed via behavior' / 'There are two cap modes' - 'tokenId is now a flat BigInt (no { integer: N } wrapper)' → 'tokenId is a flat BigInt scalar' - 'The v2 unified Freeze(... freezeType) shape no longer exists' line dropped from freezing-thawing - transferEnj keepAlive bullet rewritten to describe current shape rather than v2/v3 diff - SDK warning callout retitled from 'Platform v3 SDKs are not yet available' to 'SDKs are not yet available'; body now says examples are 'out of date and will not work against the current Enjin Platform API' instead of v2/v3 wording - 'Pending v3 confirmation' callouts in burning-destroying retitled to 'Section under review' with the same neutral framing Co-Authored-By: Claude Opus 4.7 (1M context) --- .../01-managing-tokens/01-creating-collections.md | 6 +++--- .../02-creating-a-currency-token.md | 6 +++--- .../02-creating-tokens/02-creating-tokens.md | 10 +++++----- .../01-managing-tokens/03-adding-metadata.md | 6 +++--- .../01-managing-tokens/04-minting-a-token.md | 6 +++--- .../01-managing-tokens/05-transferring-tokens.md | 8 ++++---- .../01-managing-tokens/07-freezing-thawing.md | 8 +++----- .../08-burning-destroying-tokens.md | 14 +++++++------- 8 files changed, 31 insertions(+), 33 deletions(-) diff --git a/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md b/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md index d4c91ed..c2964c3 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md +++ b/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md @@ -48,10 +48,10 @@ Once you've created a collection you're ready to start [creating tokens](/02-gui ## Option B. Using the Enjin API & SDKs -In v3, every on-chain action goes through the single `CreateTransaction` mutation. The action itself is selected by which field you set on the `transaction` input — for collections, that's `createCollection`. +Every on-chain action goes through the single `CreateTransaction` mutation. The action itself is selected by which field you set on the `transaction` input — for collections, that's `createCollection`. -:::warning Platform v3 SDKs are not yet available -The C# and C++ SDK examples below still target the Enjin Platform v2 API and **will not work against v3**. This section will be updated once the v3 SDKs ship. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +:::warning SDKs are not yet available +The C# and C++ SDK examples below are out of date and **will not work against the current Enjin Platform API**. This section will be updated once new SDKs are published. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. ::: diff --git a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md index 30f665a..f4eb416 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md +++ b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md @@ -18,7 +18,7 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa Enjin Blockchain allows you to create customized structures. This flexibility enables you to organize your tokens in various ways that suit your needs. -In v3 a token becomes a currency by setting its `behavior` field to `{ type: IS_CURRENCY, name, symbol, decimalCount }`: +A token becomes a currency by setting its `behavior` field to `{ type: IS_CURRENCY, name, symbol, decimalCount }`: - **Name:** The token name (e.g. Gold Coins). - **Symbol:** A short symbol that represents the token (e.g. GOLD). @@ -51,8 +51,8 @@ For the time being, you can create currency token via [Option B. Using the Enjin To create a currency token, use the standard `createToken` action with the `behavior` field set to `{ type: IS_CURRENCY, name, symbol, decimalCount }`. The rest of the input (recipient, collectionId, tokenId, initialSupply, etc.) follows the same shape as a regular [token create](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md#option-b-using-the-enjin-api--sdks). -:::warning Platform v3 SDKs are not yet available -The C# and C++ SDK examples below still target the Enjin Platform v2 API and **will not work against v3**. This section will be updated once the v3 SDKs ship. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +:::warning SDKs are not yet available +The C# and C++ SDK examples below are out of date and **will not work against the current Enjin Platform API**. This section will be updated once new SDKs are published. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. ::: diff --git a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md index 31eb46d..5230906 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md @@ -39,11 +39,11 @@ From here, you can configure the token's basic settings, supply cap, royalty beh - **Create Token Section -** Basic token options. Make sure to select the Collection ID you wish to mint the token in, the token ID, the initial supply, and the recipient in the corresponding fields. Make sure to check out the [TokenID Structure Best Practices](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md). -- **[Cap](/03-api-reference/04-important-arguments.md#cap) -** The token cap (if required). v3 supports two cap modes — leave the cap field unset for an unlimited supply. +- **[Cap](/03-api-reference/04-important-arguments.md#cap) -** The token cap (if required). There are two cap modes — leave the cap field unset for an unlimited supply. - **Unlimited supply (no cap)** is the default. With this model, there is no limit to how many tokens can be minted or be in circulation. The collection owner can always mint additional units, making it ideal for use cases that require an ever-expanding token supply. To pick this option, simply omit the cap field in the API, or leave the cap option unset in the dashboard form. - The **[SUPPLY](/03-api-reference/04-important-arguments.md#supply)** cap mode offers a balanced approach between flexibility and control. This model allows the collection owner to mint new tokens as long as the circulating supply does not exceed the predetermined `supply` value. Burned tokens can be re-minted, ensuring the total supply remains constant. - The **[COLLAPSING_SUPPLY](/03-api-reference/04-important-arguments.md#collapsing_supply)** cap mode is the most strict. This cap allows the collection owner to mint new tokens as long as the circulating supply does not exceed the `supply` value. However, burning tokens reduces the cap, meaning burned tokens cannot be re-minted. This ensures a non-increasing supply, suitable for use cases that require strict control over the token's total amount in circulation. -- **[Royalty Behavior](/03-api-reference/04-important-arguments.md#hasroyalty) -** Configures royalties for each marketplace sale of this token. v3 represents this via the `behavior` field on the token — set `behavior: { type: HAS_ROYALTY, royalties: [...] }`. +- **[Royalty Behavior](/03-api-reference/04-important-arguments.md#hasroyalty) -** Configures royalties for each marketplace sale of this token. Royalties are expressed via the `behavior` field on the token — set `behavior: { type: HAS_ROYALTY, royalties: [...] }`. - **[Attributes](/03-api-reference/04-important-arguments.md#attributes) -** Add details to your token using key-value pairs. Standard keys like `name` and `description` allow applications to display your content correctly. You can also use the `URI` to link to a JSON file hosting your metadata. For more information, see the [Metadata Standard](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md) page. :::info Learn more about the arguments @@ -61,10 +61,10 @@ Once your token is created, lets give it a new look by [Adding Metadata](/02-gui ## Option B. Using the Enjin API & SDKs -In v3, all on-chain actions go through the single `CreateTransaction` mutation. For creating a token, set the `createToken` field on the `transaction` input. `tokenId` is now a flat `BigInt` (no `{ integer: N }` wrapper), and `cap` is optional — omit it to create a token with unlimited supply. +All on-chain actions go through the single `CreateTransaction` mutation. For creating a token, set the `createToken` field on the `transaction` input. `tokenId` is a flat `BigInt` scalar, and `cap` is optional — omit it to create a token with unlimited supply. -:::warning Platform v3 SDKs are not yet available -The C# and C++ SDK examples below still target the Enjin Platform v2 API and **will not work against v3**. This section will be updated once the v3 SDKs ship. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +:::warning SDKs are not yet available +The C# and C++ SDK examples below are out of date and **will not work against the current Enjin Platform API**. This section will be updated once new SDKs are published. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. ::: diff --git a/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md b/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md index d348127..e81f3a7 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md +++ b/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md @@ -93,15 +93,15 @@ Since this request requires a , it must be sign ### Option B. Using the Enjin API & SDKs -In v3, batched attributes are split into two discriminator actions on `CreateTransaction`: +Batched attributes are split into two discriminator actions on `CreateTransaction`: - `batchSetTokenAttribute` — sets multiple attributes on a single token (`collectionId`, `tokenId`, `attributes`). - `batchSetCollectionAttribute` — sets multiple attributes on a collection (`id`, `attributes`). The example below sets three attributes on a token. To target the collection itself, swap `batchSetTokenAttribute` for `batchSetCollectionAttribute` and replace the `collectionId` / `tokenId` pair with a single `id` field equal to the collection ID. -:::warning Platform v3 SDKs are not yet available -The C# and C++ SDK examples below still target the Enjin Platform v2 API and **will not work against v3**. This section will be updated once the v3 SDKs ship. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +:::warning SDKs are not yet available +The C# and C++ SDK examples below are out of date and **will not work against the current Enjin Platform API**. This section will be updated once new SDKs are published. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. ::: diff --git a/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md b/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md index 44e76e5..0c9775b 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md +++ b/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md @@ -43,15 +43,15 @@ Since this request requires a , it must be sign ## Option B. Using the Enjin API & SDKs -In v3, minting is split into two discriminator actions on `CreateTransaction`: +Minting is split into two discriminator actions on `CreateTransaction`: - `mintToken` — mints to a single recipient (`recipient`, `collectionId`, `tokenId`, `amount`, optional `unitPrice`). - `mintTokens` — mints to multiple recipients in one transaction (`collectionId` + a `tokens: [MintTokenEntryInput!]` list where each entry has `recipient`, `tokenId`, `amount`, optional `unitPrice`). The example below uses `mintTokens` so it scales naturally if you add more recipients to the array. -:::warning Platform v3 SDKs are not yet available -The C# and C++ SDK examples below still target the Enjin Platform v2 API and **will not work against v3**. This section will be updated once the v3 SDKs ship. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +:::warning SDKs are not yet available +The C# and C++ SDK examples below are out of date and **will not work against the current Enjin Platform API**. This section will be updated once new SDKs are published. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. ::: diff --git a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md index 4a59fce..510b127 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md @@ -45,14 +45,14 @@ Since this request requires a , it must be sign ## Option B. Using the Enjin API & SDKs -In v3, transfers are split into three discriminator actions on `CreateTransaction`: +Transfers are split into three discriminator actions on `CreateTransaction`: - `transferToken` — transfer a specific token between accounts. - `transferEnj` — transfer ENJ (or cENJ on Canary) between accounts. - `batchTransfer` — transfer multiple tokens from one collection to multiple recipients in a single transaction. -:::warning Platform v3 SDKs are not yet available -The C# and C++ SDK examples below still target the Enjin Platform v2 API and **will not work against v3**. This section will be updated once the v3 SDKs ship. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +:::warning SDKs are not yet available +The C# and C++ SDK examples below are out of date and **will not work against the current Enjin Platform API**. This section will be updated once new SDKs are published. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. ::: ### Transferring an asset @@ -500,7 +500,7 @@ print(response.json()) - The Platform accepts ENJ values in the **base unit** (integers), not decimal ENJ amounts. To calculate the correct input, **multiply your desired ENJ amount by 10^18** (1 quintillion). - **Formula:** `Desired ENJ` \* `1,000,000,000,000,000,000` - **Example:** To transfer **5 ENJ**, input `5000000000000000000`. -- **`keepAlive` is no longer an input** — v3's `transferEnj` doesn't expose the `keepAlive` flag that v2's `TransferBalance` / `TransferKeepAlive` did. If you need to send your full balance, send slightly less than the full balance to avoid reaping the source account. [Read more about account reaping](https://support.enjin.io/hc/en-gb/articles/16297132519569-What-is-the-Existential-Deposit). +- **`keepAlive` flag** — `transferEnj` does not expose a `keepAlive` flag. If you need to send your full balance, send slightly less than the full balance to avoid reaping the source account. [Read more about account reaping](https://support.enjin.io/hc/en-gb/articles/16297132519569-What-is-the-Existential-Deposit). ::: ### Batch Transferring ENJ token diff --git a/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md b/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md index 68921f5..b2225bc 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md +++ b/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md @@ -68,17 +68,15 @@ Since this request requires a , it must be sign ## Option B. Using the Enjin API & SDKs -In v3, freeze and thaw are split into four discriminator actions on `CreateTransaction`: +Freeze and thaw are split into four discriminator actions on `CreateTransaction`: - `freezeCollection: { collectionId }` - `freezeToken: { collectionId, tokenId, state }` — where `state` is `PERMANENT`, `TEMPORARY`, or `NEVER`. - `thawCollection: { collectionId }` - `thawToken: { collectionId, tokenId, state }` -The v2 unified `Freeze(... freezeType: COLLECTION | TOKEN)` shape no longer exists. - -:::warning Platform v3 SDKs are not yet available -The C# and C++ SDK examples below still target the Enjin Platform v2 API and **will not work against v3**. This section will be updated once the v3 SDKs ship. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +:::warning SDKs are not yet available +The C# and C++ SDK examples below are out of date and **will not work against the current Enjin Platform API**. This section will be updated once new SDKs are published. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. ::: ### Freezing an entire collection diff --git a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md index 8d2a210..994eae2 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md @@ -58,8 +58,8 @@ To destroy a token, follow the above instructions for Melting a token, but make ### Destroying a collection -:::warning Pending v3 confirmation -This section is awaiting confirmation from the Enjin team on v3 support — the v3 GraphQL schema currently exposes no `destroyCollection` action. The dashboard wording below describes the v2 flow and **may or may not still be available** in the v3 dashboard. We'll refresh this section once we hear back. +:::warning Section under review +The dashboard flow for destroying a collection is being verified — the GraphQL API currently exposes no `destroyCollection` action, so the steps below may not match the current platform. We'll refresh this section once verified. ::: :::info To destroy a collection, these requirements must be met: @@ -84,10 +84,10 @@ Since this request requires a , it'll need to b ## Option B. Using the Enjin API & SDKs -In v3, burning is the `burnToken` discriminator action on `CreateTransaction`. The same action handles both "melt some supply" and "destroy the token entirely" — set `removeTokenStorage: true` to destroy. +Burning is the `burnToken` discriminator action on `CreateTransaction`. The same action handles both "melt some supply" and "destroy the token entirely" — set `removeTokenStorage: true` to destroy. -:::warning Platform v3 SDKs are not yet available -The C# and C++ SDK examples below still target the Enjin Platform v2 API and **will not work against v3**. This section will be updated once the v3 SDKs ship. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +:::warning SDKs are not yet available +The C# and C++ SDK examples below are out of date and **will not work against the current Enjin Platform API**. This section will be updated once new SDKs are published. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. ::: ### Melting token's supply @@ -459,8 +459,8 @@ Once the transaction is executed, the token will be destroyed and the Date: Wed, 13 May 2026 15:01:41 +0300 Subject: [PATCH 19/67] Document the dashboard flow for Creating a Currency Token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v2 page said the UI option was unavailable. The current dashboard does expose currency-token creation: it lives on the standard Create Token form under Advanced Settings → Other Options → Is Currency, which reveals Symbol and Decimal Count fields. Replace the 'Unavailable via the User Interface' warning with a walkthrough of that flow, linking the shared token-creation fields back to the Creating Tokens page rather than duplicating them. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../02-creating-a-currency-token.md | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md index f4eb416..2f42f55 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md +++ b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md @@ -42,11 +42,28 @@ Before minting the Mainnet versions of your Tokens, that will be used in your li ## Option A. Using the Enjin Dashboard -:::warning Unavailable via the User Interface -The option to create a currency token via the user interface is currently being developed. -For the time being, you can create currency token via [Option B. Using the Enjin API & SDKs](#option-b-using-the-enjin-api--sdks). +In the Platform menu, navigate to "**[Tokens](https://platform.beta.enjin.io/tokens)**". Then, click the "**[Create Token](https://platform.beta.enjin.io/create/token)**" button. + +Fill in the standard token fields — Collection ID, Token ID, Initial Supply, and Recipient. See [Creating Tokens](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md#option-a-using-the-enjin-dashboard) for the full breakdown of those fields. + +To mark the token as a currency, expand the **Advanced Settings** section. Under **Other Options**, tick the **Is Currency** checkbox. Two additional fields appear: + +- **Symbol -** A short symbol that represents the token (e.g. `GOLD`). +- **Decimal Count -** The number of decimal places this token should support when displayed in applications (e.g. `2` for a token like Gold Coins where balances should render with two decimals). + +:::info Learn more about the arguments +For a comprehensive view and detail of all available arguments please refer to our [API Reference](/03-api-reference/03-api-reference.md). ::: +Once you're satisfied with the options, click the "**Create**" button to submit the request. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. + +Since this request requires a , it must be signed before it broadcasts. + +- By default, transactions are signed automatically by the **Wallet Daemon**. +- To sign with a different account, expand **Transaction Options → Signing Account** on the form and provide a [Managed Wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) address. + +Once your token is created, lets give it a new look by [Adding Metadata](/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md). + ## Option B. Using the Enjin API & SDKs To create a currency token, use the standard `createToken` action with the `behavior` field set to `{ type: IS_CURRENCY, name, symbol, decimalCount }`. The rest of the input (recipient, collectionId, tokenId, initialSupply, etc.) follows the same shape as a regular [token create](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md#option-b-using-the-enjin-api--sdks). From c5e04c5162f90d828455003690d53e07b74edd49 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 13 May 2026 15:18:59 +0300 Subject: [PATCH 20/67] Document v3 dashboard flows: no Tokens page, batching via queue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v3 dashboard dropped the top-level Tokens page (404) and removed the standalone Batch button. Document these two patterns once on the Getting Started platform page, then link every managing-tokens guide to the shared explanation. Platform page (04-using-the-enjin-platform.md): - New section '5. Working with the Dashboard' (current section 5 renumbered to 6) with two stable anchors: - {#finding-tokens}: explains the Collections → click collection → click 3-dots menu flow that replaces the v2 Tokens page - {#batching-transactions}: explains '+ Add to Batch' on action forms plus the Batch Queue panel that submits everything as one CreateBatchTransaction - Image placeholders left for the user to fill in Managing-tokens guides: - 02-creating-tokens, 02-creating-a-currency-token: 'navigate to Tokens → Create Token' rewritten to 'navigate to Collections → click the collection → Create Token' - 03-adding-metadata, 04-minting-a-token, 05-transferring-tokens, 08-burning-destroying-tokens (melt): Tokens-menu navigation replaced with a link to #finding-tokens; reads as 'Open the token in the dashboard, click the 3 dots, then click ' - 03-adding-metadata, 05-transferring-tokens: 'Click the Batch button, followed by Batch ' callouts rewritten to point at + Add to Batch with a link to #batching-transactions - 07-freezing-thawing: tip about the token-level flow now links to #finding-tokens; '3 vertical dots to its right' normalized to 'on its row' - 08-burning-destroying-tokens (deferred Destroy Collection subsection): typo 'to it's right' tightened to 'on its row' Also picks up an earlier user edit on 02-creating-tokens.md clarifying that the dashboard form has an explicit 'Infinite cap type option' to select for unlimited supply. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../04-using-the-enjin-platform.md | 36 ++++++++++++++++++- .../02-creating-a-currency-token.md | 2 +- .../02-creating-tokens/02-creating-tokens.md | 4 +-- .../01-managing-tokens/03-adding-metadata.md | 5 ++- .../01-managing-tokens/04-minting-a-token.md | 3 +- .../05-transferring-tokens.md | 5 ++- .../01-managing-tokens/07-freezing-thawing.md | 6 ++-- .../08-burning-destroying-tokens.md | 5 ++- 8 files changed, 47 insertions(+), 19 deletions(-) diff --git a/docs/01-getting-started/04-using-the-enjin-platform.md b/docs/01-getting-started/04-using-the-enjin-platform.md index 78d0039..5b159aa 100644 --- a/docs/01-getting-started/04-using-the-enjin-platform.md +++ b/docs/01-getting-started/04-using-the-enjin-platform.md @@ -238,7 +238,41 @@ The response includes a `uuid` you can use to query the transaction's status as Once a request is created — whether through the dashboard or the API — the Wallet Daemon (or the [Managed Wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) you specified in the `Signing Account` field) picks it up, signs it, and broadcasts it to the chain. There is no manual approval step. The transaction will move from `PENDING` → `BROADCAST` → `FINALIZED` as it's processed. ::: -## 5. Receive Transaction Information +## 5. Working with the Dashboard + +A couple of dashboard patterns are worth knowing before you start managing existing collections and tokens. + +### Finding existing tokens {#finding-tokens} + +The dashboard does not have a top-level **Tokens** page. Tokens are always viewed inside the collection they belong to: + +1. In the Platform menu, navigate to "**[Collections](https://platform.beta.enjin.io/collections)**". +2. Click the collection that contains the token you want to manage. +3. The collection page lists every token in the collection. Click the **3 vertical dots** (**⋮**) on a token's row to open its action menu — **Mint**, **Transfer**, **Attributes**, **Burn**, **Freeze / Thaw**, and so on. + +Most managing-tokens guides describe their dashboard flow assuming you've already navigated to the token this way. + +![Placeholder: collection page with the per-token actions menu open](/img/getting-started/v3-collection-tokens-list.png) + +### Batching transactions {#batching-transactions} + +The dashboard can group several actions into a single on-chain transaction. There is no dedicated "Batch" button — instead, every batchable action form has a **+ Add to Batch** button next to its primary CTA: + +![Placeholder: Set Attribute form showing the Set Attribute, + Add to Batch, and Cancel buttons](/img/getting-started/v3-form-add-to-batch.png) + +Clicking **+ Add to Batch** queues the action instead of submitting it. Repeat for each action you want to bundle. + +When you're ready to submit, open the **Batch Queue** panel from the bottom-right corner of the dashboard. The panel lists every queued action with a **Remove** link, lets you pick a **Signing Account** (defaults to the Wallet Daemon), and submits the whole list as a single `CreateBatchTransaction` when you click **Submit Batch**: + +![Placeholder: Batch Queue panel with two queued actions and the Submit Batch button](/img/getting-started/v3-batch-queue.png) + +A successful submit produces a single transaction `uuid` on the [Transactions](https://platform.beta.enjin.io/transactions) page — the same shape any other transaction uses, just with multiple actions packed into it. + +:::tip Programmatic equivalent +The API equivalent of the Batch Queue is the `CreateBatchTransaction` mutation — it takes a `transactions: [TransactionInput!]!` list where each entry is one of the same discriminator inputs you'd pass to `CreateTransaction`. +::: + +## 6. Receive Transaction Information There are 3 ways to receive the transaction status and information: diff --git a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md index 2f42f55..6f804bb 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md +++ b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md @@ -42,7 +42,7 @@ Before minting the Mainnet versions of your Tokens, that will be used in your li ## Option A. Using the Enjin Dashboard -In the Platform menu, navigate to "**[Tokens](https://platform.beta.enjin.io/tokens)**". Then, click the "**[Create Token](https://platform.beta.enjin.io/create/token)**" button. +In the Platform menu, navigate to "**[Collections](https://platform.beta.enjin.io/collections)**" and click the collection you want to mint the token into. From the collection page, click the "**Create Token**" button. Fill in the standard token fields — Collection ID, Token ID, Initial Supply, and Recipient. See [Creating Tokens](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md#option-a-using-the-enjin-dashboard) for the full breakdown of those fields. diff --git a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md index 5230906..6e25c28 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md @@ -33,14 +33,14 @@ Before minting the Mainnet versions of your Tokens, that will be used in your li ## Option A. Using the Enjin Dashboard -In the Platform menu, navigate to "**[Tokens](https://platform.beta.enjin.io/tokens)**". Then, click the "**[Create Token](https://platform.beta.enjin.io/create/token)**" button. +In the Platform menu, navigate to "**[Collections](https://platform.beta.enjin.io/collections)**" and click the collection you want to mint the token into. From the collection page, click the "**Create Token**" button. From here, you can configure the token's basic settings, supply cap, royalty behavior, and attributes. - **Create Token Section -** Basic token options. Make sure to select the Collection ID you wish to mint the token in, the token ID, the initial supply, and the recipient in the corresponding fields. Make sure to check out the [TokenID Structure Best Practices](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md). - **[Cap](/03-api-reference/04-important-arguments.md#cap) -** The token cap (if required). There are two cap modes — leave the cap field unset for an unlimited supply. - - **Unlimited supply (no cap)** is the default. With this model, there is no limit to how many tokens can be minted or be in circulation. The collection owner can always mint additional units, making it ideal for use cases that require an ever-expanding token supply. To pick this option, simply omit the cap field in the API, or leave the cap option unset in the dashboard form. + - **Unlimited supply (no cap)** is the default. With this model, there is no limit to how many tokens can be minted or be in circulation. The collection owner can always mint additional units, making it ideal for use cases that require an ever-expanding token supply. To pick this option, simply omit the cap field in the API, or select the Infinite cap type option in the dashboard form. - The **[SUPPLY](/03-api-reference/04-important-arguments.md#supply)** cap mode offers a balanced approach between flexibility and control. This model allows the collection owner to mint new tokens as long as the circulating supply does not exceed the predetermined `supply` value. Burned tokens can be re-minted, ensuring the total supply remains constant. - The **[COLLAPSING_SUPPLY](/03-api-reference/04-important-arguments.md#collapsing_supply)** cap mode is the most strict. This cap allows the collection owner to mint new tokens as long as the circulating supply does not exceed the `supply` value. However, burning tokens reduces the cap, meaning burned tokens cannot be re-minted. This ensures a non-increasing supply, suitable for use cases that require strict control over the token's total amount in circulation. - **[Royalty Behavior](/03-api-reference/04-important-arguments.md#hasroyalty) -** Configures royalties for each marketplace sale of this token. Royalties are expressed via the `behavior` field on the token — set `behavior: { type: HAS_ROYALTY, royalties: [...] }`. diff --git a/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md b/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md index e81f3a7..9800859 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md +++ b/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md @@ -75,11 +75,10 @@ Ensure you use the correct casing when defining attributes to avoid errors. ### Option A. Using the Enjin Dashboard -In the Platform menu, navigate to "**[Tokens](https://platform.beta.enjin.io/tokens)**". -**Locate the token** you wish to add / edit attributes for, click the **3 vertical dots** (**⋮**) to its right, then click the "**Attributes**" button. +[Open the token in the dashboard](/01-getting-started/04-using-the-enjin-platform.md#finding-tokens), click the **3 vertical dots** (**⋮**) on its row, then click "**Attributes**". :::tip Need to add multiple attributes for a token? -Click on the "**Batch**" button, followed by "**Batch SetAttribute**". +Each Set Attribute form has a **+ Add to Batch** button next to **Set Attribute** — queue several attributes and submit them as a single batched transaction. See [Batching transactions](/01-getting-started/04-using-the-enjin-platform.md#batching-transactions) for the full flow. ::: To add / edit an attribute, select the "**Set**" option, type in the "**Key**" that you wish to add / edit, and its "**Value**" in the corresponding text fields. diff --git a/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md b/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md index 0c9775b..950a335 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md +++ b/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md @@ -29,8 +29,7 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa ## Option A. Using the Enjin Dashboard -In the Platform menu, navigate to "**[Tokens](https://platform.beta.enjin.io/tokens)**". -**Locate the token** you wish to mint, click the **3 vertical dots** (**⋮**) to its right, then click the "**Mint**" button. +[Open the token in the dashboard](/01-getting-started/04-using-the-enjin-platform.md#finding-tokens), click the **3 vertical dots** (**⋮**) on its row, then click "**Mint**". Set the recipient and the amount in the corresponding fields, and click on "**Mint**". diff --git a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md index 510b127..7b4a36e 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md @@ -28,11 +28,10 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa ## Option A. Using the Enjin Dashboard -In the Platform menu, navigate to "**[Tokens](https://platform.beta.enjin.io/tokens)**". -**Locate the token** you wish to transfer, click the **3 vertical dots** (**⋮**) to its right, then click the "**Transfer**" button. +[Open the token in the dashboard](/01-getting-started/04-using-the-enjin-platform.md#finding-tokens), click the **3 vertical dots** (**⋮**) on its row, then click "**Transfer**". :::info Need to perform multiple transfers? -Click on the "**Batch**" button, followed by "**Batch Transfer**". +The Transfer form has a **+ Add to Batch** button next to **Transfer** — queue several transfers and submit them as a single batched transaction. See [Batching transactions](/01-getting-started/04-using-the-enjin-platform.md#batching-transactions) for the full flow. ::: Fill in the recipient, amount, and other optional arguments in the corresponding fields. diff --git a/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md b/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md index b2225bc..754c99a 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md +++ b/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md @@ -47,15 +47,13 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa ## Option A. Using the Enjin Dashboard :::tip Applying Freeze/Thaw Actions to Collections and Tokens -This tutorial illustrates the process of freezing a collection. -However, the same steps can be applied to freeze or thaw tokens. -Simply navigate to the corresponding menu for tokens instead of collections, or for thawing instead of freezing. +This tutorial illustrates the process of freezing a collection. The same flow applies to freezing or thawing a single token — [open the token in the dashboard](/01-getting-started/04-using-the-enjin-platform.md#finding-tokens) and pick **Freeze** or **Thaw** from its actions menu instead. ::: ### Freezing an entire collection In the Platform menu, navigate to "**[Collections](https://platform.beta.enjin.io/collections)**". -**Locate the collection** you wish to freeze, click the **3 vertical dots** (**⋮**) to its right, then click the "**Freeze**" button. +**Locate the collection** you wish to freeze, click the **3 vertical dots** (**⋮**) on its row, then click the "**Freeze**" button. Choose the freeze state, and click on the "**Freeze**" button. diff --git a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md index 994eae2..e25f679 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md @@ -27,8 +27,7 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa ### Melting token's supply -In the Platform menu, navigate to "**[Tokens](https://platform.beta.enjin.io/tokens)**". -**Locate the token** you wish to melt, click the **3 vertical dots** (**⋮**) to its right, then click the "**Burn**" button. +[Open the token in the dashboard](/01-getting-started/04-using-the-enjin-platform.md#finding-tokens), click the **3 vertical dots** (**⋮**) on its row, then click "**Burn**". Insert the amount of tokens to melt, and click on the "**Burn**" button. @@ -71,7 +70,7 @@ The dashboard flow for destroying a collection is being verified — the GraphQL ::: In the Platform menu, navigate to "**[Collections](https://platform.beta.enjin.io/collections)**". -**Locate the collection** you wish to destroy, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Destroy**" button. +**Locate the collection** you wish to destroy, click the **3 vertical dots** (**⋮**) on its row, then click the "**Destroy**" button. Then, confirm by clicking the "**Destroy**" button From 00dd0ccbc8bb7e8968f1f383f4eb2818e3abc59f Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 18 May 2026 17:41:31 +0300 Subject: [PATCH 21/67] Add v3 dashboard screenshots and refine UI instructions Fill in the image placeholders left on the platform and managing-tokens pages with real v3 dashboard screenshots, and tighten the dashboard walkthroughs to match the live v3 UI: - 04-using-the-enjin-platform: drop placeholder captions on the collection-tokens-list, add-to-batch, and batch-queue images - 03-adding-metadata: action menu item is "Set Attribute"; add the Set Attribute form screenshot - 04-minting-a-token: action menu opens a form with a "Mint Token" submit button; add the Mint Token form screenshot; trim the Play-to-Mint preamble - 05-transferring-tokens: add the Transfer Token form screenshot - 07-freezing-thawing: collection action is "Freeze Collection", which opens a form with its own "Freeze Collection" submit button; token actions are "Freeze Token" / "Thaw Token"; add the Freeze Collection form screenshot - 08-burning-destroying-tokens: action menu item and submit button are "Burn Token"; attribute removal is per-attribute via "Remove Attribute" (no Remove All); add the Burn Token form screenshot; "checkbox" wording - 02-creating-tokens, 02-creating-a-currency-token: example tokenId bumped 0 -> 1 across all language tabs - freezing-thawing: freezing applies to transfers (melting wording dropped from the scope note) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../04-using-the-enjin-platform.md | 10 ++++------ .../02-creating-a-currency-token.md | 12 ++++++------ .../02-creating-tokens/02-creating-tokens.md | 14 +++++++------- .../01-managing-tokens/03-adding-metadata.md | 10 ++++++---- .../01-managing-tokens/04-minting-a-token.md | 9 +++------ .../05-transferring-tokens.md | 9 ++++++--- .../01-managing-tokens/07-freezing-thawing.md | 8 ++++---- .../08-burning-destroying-tokens.md | 12 +++++++----- static/img/getting-started/v3-batch-queue.png | Bin 0 -> 31139 bytes .../img/getting-started/v3-burn-token-form.png | Bin 0 -> 22644 bytes .../v3-collection-tokens-list.png | Bin 0 -> 351288 bytes .../getting-started/v3-form-add-to-batch.png | Bin 0 -> 19807 bytes .../v3-freeze-collection-form.png | Bin 0 -> 14541 bytes .../img/getting-started/v3-mint-token-form.png | Bin 0 -> 23240 bytes .../getting-started/v3-set-attribute-form.png | Bin 0 -> 20014 bytes .../getting-started/v3-transfer-token-form.png | Bin 0 -> 23398 bytes 16 files changed, 43 insertions(+), 41 deletions(-) create mode 100644 static/img/getting-started/v3-batch-queue.png create mode 100644 static/img/getting-started/v3-burn-token-form.png create mode 100644 static/img/getting-started/v3-collection-tokens-list.png create mode 100644 static/img/getting-started/v3-form-add-to-batch.png create mode 100644 static/img/getting-started/v3-freeze-collection-form.png create mode 100644 static/img/getting-started/v3-mint-token-form.png create mode 100644 static/img/getting-started/v3-set-attribute-form.png create mode 100644 static/img/getting-started/v3-transfer-token-form.png diff --git a/docs/01-getting-started/04-using-the-enjin-platform.md b/docs/01-getting-started/04-using-the-enjin-platform.md index 5b159aa..989dc5b 100644 --- a/docs/01-getting-started/04-using-the-enjin-platform.md +++ b/docs/01-getting-started/04-using-the-enjin-platform.md @@ -250,21 +250,19 @@ The dashboard does not have a top-level **Tokens** page. Tokens are always viewe 2. Click the collection that contains the token you want to manage. 3. The collection page lists every token in the collection. Click the **3 vertical dots** (**⋮**) on a token's row to open its action menu — **Mint**, **Transfer**, **Attributes**, **Burn**, **Freeze / Thaw**, and so on. -Most managing-tokens guides describe their dashboard flow assuming you've already navigated to the token this way. - -![Placeholder: collection page with the per-token actions menu open](/img/getting-started/v3-collection-tokens-list.png) +![Collection page with the per-token actions menu open](/img/getting-started/v3-collection-tokens-list.png) ### Batching transactions {#batching-transactions} -The dashboard can group several actions into a single on-chain transaction. There is no dedicated "Batch" button — instead, every batchable action form has a **+ Add to Batch** button next to its primary CTA: +The dashboard can group several actions into a single on-chain transaction. Every batchable action form has a **+ Add to Batch** button next to its primary submit button: -![Placeholder: Set Attribute form showing the Set Attribute, + Add to Batch, and Cancel buttons](/img/getting-started/v3-form-add-to-batch.png) +![Set Attribute form showing the Set Attribute, + Add to Batch, and Cancel buttons](/img/getting-started/v3-form-add-to-batch.png) Clicking **+ Add to Batch** queues the action instead of submitting it. Repeat for each action you want to bundle. When you're ready to submit, open the **Batch Queue** panel from the bottom-right corner of the dashboard. The panel lists every queued action with a **Remove** link, lets you pick a **Signing Account** (defaults to the Wallet Daemon), and submits the whole list as a single `CreateBatchTransaction` when you click **Submit Batch**: -![Placeholder: Batch Queue panel with two queued actions and the Submit Batch button](/img/getting-started/v3-batch-queue.png) +![Batch Queue panel with two queued actions and the Submit Batch button](/img/getting-started/v3-batch-queue.png) A successful submit produces a single transaction `uuid` on the [Transactions](https://platform.beta.enjin.io/transactions) page — the same shape any other transaction uses, just with multiple actions packed into it. diff --git a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md index 6f804bb..398bb4b 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md +++ b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md @@ -83,7 +83,7 @@ mutation CreateCurrencyToken { createToken: { recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" # recipient of the initial supply collectionId: 2406 # collection to mint into - tokenId: 0 # the new token ID + tokenId: 1 # the new token ID initialSupply: 1 # initial supply to mint listingForbidden: false infusion: 0 @@ -109,7 +109,7 @@ mutation CreateCurrencyToken { curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_API_TOKEN' \ --d '{"query":"mutation CreateCurrencyToken($recipient: String!, $collectionId: BigInt!, $tokenId: BigInt!, $initialSupply: BigInt!, $behavior: TokenBehaviorInput!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: {\r\n createToken: {\r\n recipient: $recipient\r\n collectionId: $collectionId\r\n tokenId: $tokenId\r\n initialSupply: $initialSupply\r\n listingForbidden: false\r\n infusion: 0\r\n anyoneCanInfuse: false\r\n behavior: $behavior\r\n }\r\n }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","collectionId":2406,"tokenId":0,"initialSupply":1,"behavior":{"type":"IS_CURRENCY","name":"Gold Coins","symbol":"GOLD","decimalCount":2}}}' +-d '{"query":"mutation CreateCurrencyToken($recipient: String!, $collectionId: BigInt!, $tokenId: BigInt!, $initialSupply: BigInt!, $behavior: TokenBehaviorInput!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: {\r\n createToken: {\r\n recipient: $recipient\r\n collectionId: $collectionId\r\n tokenId: $tokenId\r\n initialSupply: $initialSupply\r\n listingForbidden: false\r\n infusion: 0\r\n anyoneCanInfuse: false\r\n behavior: $behavior\r\n }\r\n }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","collectionId":2406,"tokenId":1,"initialSupply":1,"behavior":{"type":"IS_CURRENCY","name":"Gold Coins","symbol":"GOLD","decimalCount":2}}}' ```
@@ -125,7 +125,7 @@ var tokenMetadata = new TokenMetadataInput() // Define the token parameters var tokenParams = new CreateTokenParams() - .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) //Set the token ID + .SetTokenId(new EncodableTokenIdInput().SetInteger(1)) //Set the token ID .SetInitialSupply(1) //Mint initial supply .SetCap(new TokenMintCap().SetType(TokenMintCapType.Infinite)) //Define supply type .SetMetadata(tokenMetadata); //Set the token metadata @@ -201,7 +201,7 @@ fetch('https://platform.beta.enjin.io/graphql', { variables: { recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", collectionId: 2406, - tokenId: 0, + tokenId: 1, initialSupply: 1, behavior: { type: "IS_CURRENCY", name: "Gold Coins", symbol: "GOLD", decimalCount: 2 } } @@ -249,7 +249,7 @@ axios.post('https://platform.beta.enjin.io/graphql', { variables: { recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", collectionId: 2406, - tokenId: 0, + tokenId: 1, initialSupply: 1, behavior: { type: "IS_CURRENCY", name: "Gold Coins", symbol: "GOLD", decimalCount: 2 } } @@ -298,7 +298,7 @@ mutation CreateCurrencyToken( variables = { 'recipient': 'cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f', 'collectionId': 2406, - 'tokenId': 0, + 'tokenId': 1, 'initialSupply': 1, 'behavior': {'type': 'IS_CURRENCY', 'name': 'Gold Coins', 'symbol': 'GOLD', 'decimalCount': 2}, } diff --git a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md index 6e25c28..67e96d1 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md @@ -78,7 +78,7 @@ mutation CreateToken { createToken: { recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" # recipient of the initial supply collectionId: 2406 # collection to mint into - tokenId: 0 # the new token ID + tokenId: 1 # the new token ID initialSupply: 1 # initial supply to mint listingForbidden: false # set true to disallow marketplace listings infusion: 0 # ENJ infused per unit (use BigInt base units) @@ -100,7 +100,7 @@ mutation CreateToken { curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_API_TOKEN' \ --d '{"query":"mutation CreateToken($recipient: String!, $collectionId: BigInt!, $tokenId: BigInt!, $initialSupply: BigInt!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: {\r\n createToken: {\r\n recipient: $recipient\r\n collectionId: $collectionId\r\n tokenId: $tokenId\r\n initialSupply: $initialSupply\r\n listingForbidden: false\r\n infusion: 0\r\n anyoneCanInfuse: false\r\n }\r\n }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","collectionId":2406,"tokenId":0,"initialSupply":1}}' +-d '{"query":"mutation CreateToken($recipient: String!, $collectionId: BigInt!, $tokenId: BigInt!, $initialSupply: BigInt!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: {\r\n createToken: {\r\n recipient: $recipient\r\n collectionId: $collectionId\r\n tokenId: $tokenId\r\n initialSupply: $initialSupply\r\n listingForbidden: false\r\n infusion: 0\r\n anyoneCanInfuse: false\r\n }\r\n }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","collectionId":2406,"tokenId":1,"initialSupply":1}}' ``` @@ -110,7 +110,7 @@ using Enjin.Platform.Sdk; // Define the token parameters var tokenParams = new CreateTokenParams() - .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) //Set the token ID + .SetTokenId(new EncodableTokenIdInput().SetInteger(1)) //Set the token ID .SetInitialSupply(1) //Mint initial supply .SetCap(new TokenMintCap().SetType(TokenMintCapType.Infinite)); //Define supply type @@ -155,7 +155,7 @@ int main() { shared_ptr tokenParams = make_shared(); shared_ptr tokenId = make_shared(); - tokenId->SetInteger(make_shared("0")); + tokenId->SetInteger(make_shared("1")); shared_ptr tokenMintCap = make_shared(); tokenMintCap->SetType(TokenMintCapType::Infinite); @@ -259,7 +259,7 @@ fetch('https://platform.beta.enjin.io/graphql', { variables: { recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", collectionId: 2406, - tokenId: 0, + tokenId: 1, initialSupply: 1 } }), @@ -304,7 +304,7 @@ axios.post('https://platform.beta.enjin.io/graphql', { variables: { recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", collectionId: 2406, - tokenId: 0, + tokenId: 1, initialSupply: 1 } }, { @@ -350,7 +350,7 @@ mutation CreateToken( variables = { 'recipient': 'cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f', 'collectionId': 2406, - 'tokenId': 0, + 'tokenId': 1, 'initialSupply': 1, } diff --git a/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md b/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md index 9800859..396e496 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md +++ b/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md @@ -75,13 +75,15 @@ Ensure you use the correct casing when defining attributes to avoid errors. ### Option A. Using the Enjin Dashboard -[Open the token in the dashboard](/01-getting-started/04-using-the-enjin-platform.md#finding-tokens), click the **3 vertical dots** (**⋮**) on its row, then click "**Attributes**". +[Locate the token in the dashboard](/01-getting-started/04-using-the-enjin-platform.md#finding-tokens), click the **3 vertical dots** (**⋮**), then click "**Set Attribute**". -:::tip Need to add multiple attributes for a token? -Each Set Attribute form has a **+ Add to Batch** button next to **Set Attribute** — queue several attributes and submit them as a single batched transaction. See [Batching transactions](/01-getting-started/04-using-the-enjin-platform.md#batching-transactions) for the full flow. +:::info Need to add multiple attributes for a token? +The Set Attribute form has a **+ Add to Batch** button — queue several attributes and submit them as a single batched transaction. See [Batching transactions](/01-getting-started/04-using-the-enjin-platform.md#batching-transactions) for the full flow. ::: -To add / edit an attribute, select the "**Set**" option, type in the "**Key**" that you wish to add / edit, and its "**Value**" in the corresponding text fields. +Type in the "**Key**" that you wish to add, and its "**Value**" in the corresponding text fields. + +![The Set Attribute form](/img/getting-started/v3-set-attribute-form.png) Once you're satisfied with the options, click the "**Set Attribute**" button to submit the request. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. diff --git a/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md b/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md index 950a335..4458bec 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md +++ b/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md @@ -10,10 +10,6 @@ import TabItem from '@theme/TabItem'; Now that you've got your tokens created, it's time to start minting them into player wallets as they are earned. -We call this "Play-to-Mint," giving players the power to create tokens themselves, which feels more rewarding. - -Plus, it keeps your processes more efficient by delivering tokens right to players' wallets without any unnecessary `transfer` transactions. - :::info What you'll need: - Some [ Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for and a deposit of 0.01 ENJ is required for the , for each new token holder. If the token has , each new unit minted will require the same amount of ENJ to be infused. @@ -29,9 +25,10 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa ## Option A. Using the Enjin Dashboard -[Open the token in the dashboard](/01-getting-started/04-using-the-enjin-platform.md#finding-tokens), click the **3 vertical dots** (**⋮**) on its row, then click "**Mint**". +[Locate the token in the dashboard](/01-getting-started/04-using-the-enjin-platform.md#finding-tokens), click the **3 vertical dots** (**⋮**), then click "**Mint**". +Set the recipient and the amount in the corresponding fields, and click on "**Mint Token**". -Set the recipient and the amount in the corresponding fields, and click on "**Mint**". +![The Mint Token form](/img/getting-started/v3-mint-token-form.png) The Transaction Request will then appear in the "**Transactions**" menu. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. diff --git a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md index 7b4a36e..449f875 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md @@ -28,13 +28,16 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa ## Option A. Using the Enjin Dashboard -[Open the token in the dashboard](/01-getting-started/04-using-the-enjin-platform.md#finding-tokens), click the **3 vertical dots** (**⋮**) on its row, then click "**Transfer**". +[Locate the token in the dashboard](/01-getting-started/04-using-the-enjin-platform.md#finding-tokens), click the **3 vertical dots** (**⋮**), then click "**Transfer**". :::info Need to perform multiple transfers? -The Transfer form has a **+ Add to Batch** button next to **Transfer** — queue several transfers and submit them as a single batched transaction. See [Batching transactions](/01-getting-started/04-using-the-enjin-platform.md#batching-transactions) for the full flow. +The Transfer form has a **+ Add to Batch** button — queue several transfers and submit them as a single batched transaction. See [Batching transactions](/01-getting-started/04-using-the-enjin-platform.md#batching-transactions) for the full flow. ::: -Fill in the recipient, amount, and other optional arguments in the corresponding fields. +Fill in the recipient and amount in the corresponding fields. + +![The Transfer Token form](/img/getting-started/v3-transfer-token-form.png) + Once you're satisfied with the options, click the "**Transfer**" button to submit the request. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. Since this request requires a , it must be signed before it broadcasts. diff --git a/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md b/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md index 754c99a..de11602 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md +++ b/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md @@ -13,7 +13,7 @@ import TabItem from '@theme/TabItem'; One such use-case is the implementation of "Soulbound" tokens. A Soulbound token is bound to a specific address and cannot be transferred out of the wallet it's minted on. This feature can be used to create unique gameplay mechanics, loyalty rewards, and more. :::note -Freezing only applies to ing and transfers, which also results in marketplace listings being unpurchasable. +Freezing only applies to transfers, which also results in marketplace listings being unpurchasable. Freezing does not suspend token minting. ::: @@ -47,15 +47,15 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa ## Option A. Using the Enjin Dashboard :::tip Applying Freeze/Thaw Actions to Collections and Tokens -This tutorial illustrates the process of freezing a collection. The same flow applies to freezing or thawing a single token — [open the token in the dashboard](/01-getting-started/04-using-the-enjin-platform.md#finding-tokens) and pick **Freeze** or **Thaw** from its actions menu instead. +This tutorial illustrates the process of freezing a collection. The same flow applies to freezing or thawing a single token — [Locate the token in the dashboard](/01-getting-started/04-using-the-enjin-platform.md#finding-tokens) and pick **Freeze Token** or **Thaw Token** from its actions menu instead. ::: ### Freezing an entire collection In the Platform menu, navigate to "**[Collections](https://platform.beta.enjin.io/collections)**". -**Locate the collection** you wish to freeze, click the **3 vertical dots** (**⋮**) on its row, then click the "**Freeze**" button. +**Locate the collection** you wish to freeze, click the **3 vertical dots** (**⋮**) on its row, then click the "**Freeze Collection**" button. In the form that opens up, click on the "**Freeze Collection**" button. -Choose the freeze state, and click on the "**Freeze**" button. +![The Freeze Collection form](/img/getting-started/v3-freeze-collection-form.png) The Transaction Request will then appear in the "**Transactions**" menu. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. diff --git a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md index e25f679..6266e69 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md @@ -27,9 +27,11 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa ### Melting token's supply -[Open the token in the dashboard](/01-getting-started/04-using-the-enjin-platform.md#finding-tokens), click the **3 vertical dots** (**⋮**) on its row, then click "**Burn**". +[Locate the token in the dashboard](/01-getting-started/04-using-the-enjin-platform.md#finding-tokens), click the **3 vertical dots** (**⋮**), then click "**Burn Token**". -Insert the amount of tokens to melt, and click on the "**Burn**" button. +![The Burn Token form](/img/getting-started/v3-burn-token-form.png) + +Insert the amount of tokens to melt, and click on the "**Burn Token**" button. The Transaction Request will then appear in the "**Transactions**" menu. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. @@ -43,7 +45,7 @@ Since this request requires a , it must be sign :::info To destroy a token, these requirements must be met: - The caller is the collection owner - The token has no attributes - - If the token has attributes, you can remove the attributes by clicking the **3 vertical dots** (**⋮**) next to the token, followed by "**Attributes**" and selecting "**Remove All"**. + - If the token has attributes, you can remove the attributes by clicking the **3 vertical dots** (**⋮**) next to the token, followed by "**Remove Attribute**", selecting the attribute to remove and submitting the transaction. This needs to be done for all attributes. - The token has 0 supply - If the token has supply, you can follow the above guide [Burning token's supply](#melting-tokens-supply) to remove all token supply (as long as you own all of the token's supply) Note - you can remove the supply and destroy the token in the same melt transaction. @@ -53,7 +55,7 @@ Melting a token and destroying it are two different actions. The action demonstrated above is the action of melting a token, which decreases it's circulating supply. While destroying a token removes the token from the blockchain, and retrieves the to the collection owner. -To destroy a token, follow the above instructions for Melting a token, but make sure to tick the `Remove Token Storage` box. +To destroy a token, follow the above instructions for Melting a token, but make sure to tick the `Remove Token Storage` checkbox. ### Destroying a collection @@ -64,7 +66,7 @@ The dashboard flow for destroying a collection is being verified — the GraphQL :::info To destroy a collection, these requirements must be met: - The caller is the collection owner - The collection has no attributes - - If the collection has attributes, you can remove the attributes by clicking the **3 vertical dots** (**⋮**) next to the collection, followed by "**Attributes**" and selecting "**Remove All"**. + - If the collection has attributes, you can remove the attributes by clicking the **3 vertical dots** (**⋮**) next to the collection, followed by "**Remove Attribute**", selecting the attribute to remove and submitting the transaction. This needs to be done for all attributes. - The collection has 0 tokens **in storage** - If the collection has some tokens, you can follow the above guide [Destroying a token and removing it from the Blockchain](#destroying-a-token-and-removing-it-from-the-blockchain) for each of the tokens in the collection, to destroy them all. ::: diff --git a/static/img/getting-started/v3-batch-queue.png b/static/img/getting-started/v3-batch-queue.png new file mode 100644 index 0000000000000000000000000000000000000000..316f3e4c5a346bc3e48d474c17e0d6990c11e1c9 GIT binary patch literal 31139 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 A-2eap literal 0 HcmV?d00001 diff --git a/static/img/getting-started/v3-burn-token-form.png b/static/img/getting-started/v3-burn-token-form.png new file mode 100644 index 0000000000000000000000000000000000000000..bdab5c9df5d2aace30b9c28b93e2a04829229130 GIT binary patch literal 22644 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 @q)R0~t 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%0AY~=_ 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

qJ=Ccr@HILI8w)wXVguqT{*TyhpGb5D0B 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ΝP1&{OaE_4sdFSKgY@vSjFwQYQ*|4mG7fS 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 mA%e>DIOKvSiDfwMwv4QX?yf)=D^c_AgfuI z7~o!4X>QF(axPzk$Raj8T+yQSgepx%k>9bW=MpVY!%4| z*}&H`PzdXXZ64Iu!|?zzJ2p16&v-P-W3cuD1a$`XbOKzLw|672L(m%L-OSC ziE+|&#!uR?iuYb~@g}*?g|%MJ@5_mhzgAt2{D3T9{`1ndcLo0+OmlzFlW0JZg*=%r zLVeyiIu%u@t;baSm?6>Iw{}xiG?evF^*w%%N|u3&p^C4REE|)jd};UMJH;i`B`a~w zca7k`@Kg3Bcg@{V{!NX4$u06T-UypY(I|)>MGlksACH3xQ*j}4bk9NocH?oio>yFd zj7ofdflU5Gp1N!2O*%h3vVB9?wjZ#F1!O*qXf*Mf5`h`i=SPRoAT(SflXeea$6Yck z;Opj8(_Qh96Vw#({lb~qNMtg`R*9~Js=wUzUzR0l}umj_FE4Z`tFr`t&SHaaIS0mKo| zgI#jEuZXo>SQ}`i2b+ZH-Ws0Ws^NeNmO8m#Dpm1n_*u_uzwjV+E{QR$A9(@6y4Khx z>i4fAD#=ybz`JYXX5r9+%yr!%b<%zXEMd2{=j^axEM>A{t)XNxSAu@Reb(9D`ONKf zACW?qyZ{dWy=LFHsG4Lu=9+6R5fMxC?9G3arvH{lU;kXcx?n1K=xmg7Z55e|lV}>z z1%)mTfE&t1XDf3)+ga5Q`x~_wqXmSXC_>TBv^4$LMYJpRSU83-%0@%UF`L*AX z!|Qy*ahp?lFNzu`olcHgG_|XJ-zIdk<|xO@k$zZ~th$Ul$8d3b#yPki-Wf8FIXph| z!R|Gw3m(JAcRoyw75|=88X1$=6)(;@aj4KrIenzekMf4`=Oio$P&K@=AaJr56Xpm) z^d%H7%Kw?OAM5rjs6-EoC~OSIT{y-rHXWO)L`eRc%0*CRiv&vO5?Z72Z@$clJ2nYu zhesJls&w?uLu^T6~rsC}edLn1A zc=010!F2cXVip+YM(m=Uy^}+%Yap6Ez&|d8uF)jW+rKpKx2i8ccOD45*fWqlV-4VA z&IEda@b`ho^c7S24-~hrO)3P0Qqne>5S#^iMvY8|Ql#u0ny+i82j&#xs*Rf;_TaHG z_+L|wh85Y$;D&+~6=KCq3mGl8nBgO*@OxcuReBiHrEdJY85QxDe+2xOh%NXpX+E2~ z6QW26x~F8#Aajp$GG+HbxZX0<<9WDbpj)QZpOA40$6P_g%d>w}#2t@R2 zsst;saP|_Pt#pPz^At-DEL6A(?{C-d9Cf=JOi2g#XBP}zEOdp`uX8Ju{!#8pIlDMX z`qo+}$md2{f%?3})Stv*&j0WQ27d{&*yc#G0#e=@hXs(+32!k>m4i(k)K|s{ z8Xqe4ZvK&EZVJd*cwJ-7m1l6Q6zkFALf2&iGk|gAnOC&w2oMQNKN`|~&baba{WW2> z|98i*8_Yf)_&0Aaau3;Uw#gJdo?tO#kM-zyOox9=cUNGA{urxt5L{E7n(rJfJou}k zOE&`Tx_zClGP^l_tt+o^^Pa6%&7xAhVee077|tWFlO1Og{QZ4z|5vRpF1mp|4K~BW zSQ)~_(al_Z2xAqm$c|g&S5IR6$hDBl>0G{?@ZvE`1d^ITiEp&ytafN<5C|BW_B_o1 zZ?aSP-Do8EH){}gbaW3K++W|Kz=>@9buQj)u=rG8l^7yNo{Fm~Ha61hD9wpaF< zCe9}4;X%T$;~Y2gKEkF$RG#n?;h>#Zp2%1T=A*@3j-k3D!vb^su)pD0;~aPWZyhHn z*flam%z{8U;y{=x!5^t0#f@T}c;h@z)x*;#bLr_J=TrpjETFe-Oy3mBSjQL2KW32E zBkZhbtrgwN^2oDLsN&jcyu@8>jt+7aYr9KxG@^8AQj696jNWJaaUP}zmaz#E9GW2& zRBY&N-1#rXK^531wkeJOc~^WTJAsiWpo+)H_Vm=T55C}hO_S5PKl|;>&Rg(0Sq>r= zgt=qSl{p>VXnTYImmDAsMb-xsCG15$z`Kfq1KHl;9tS^TlR2Lb+x0wC)FlWN41^3t zJDdhD_$+McY5P9UvoUAB&6ia7n0`tE=C3%JpA$gNRfN@pD?IH?hp%g&)S4t;FTvw9 zI>}sBadIg{>5qC+Y8bR)w#gc{-4}0=Udi5N`z?E1;_&85y#&AbW?Q2n;j@yAZ_z!< zUzsiR6NNEtm8&1AHb?4wjaHJ<@YABe59Wr4M*zCdZi$#{ag_dZ*R(gxvPimTMjs~E zDAkF=t?Ywh;fe6k-P-H|=X1Lau;9`cJ;&m5M%xEc_AS9(*tJnJ{5xi!S2NnOe4HJt zc4z-p;@?64`}jokM#1b#jnB(*yUAJ`RO@@_Y!KxClSWU~>;!&bOj|2DMjU!64rf*k z!}rlM@xRN|)}OxCzndl-B3+_`pl!Yr;GAg_NIVYo4@9BY7CV)*G6}rH~AZ!h$1^O(_LYoZ1Z2UqJj9c@K}00q;))VxQ?8T7655>?cEf?w2N_d2uTSpk}=vW#7ZcTSV4; zbPkwM!l`m*gq3qzpm@n6`oXy<{F2V2*e)D5!g=1;ULlCz{+Jq|#@zPx9Lv9MaM|z> zwDloV0e_+XdHF4Ca&DH3a+q<#8VJoi>6&Uv2JCC0r1vP=MJAMzE%EPDmPSTp$q0X` zZ&da_xcw25udT25cK~r*a<^f#DoMUCcFB7cY%qb5e}l@ZEH)=65{PRvtLuGc&1DBU&}?|f+P3XgMU$ZD@tYg zs)tuptPSY;d2w6TFH~=w9vtwwG(QrbFZ;aj_p={E`)0$s?*ko+o5@->@fgB!L#re1 zmN{)k3J`NojpN@uk6t0N0!>D}U^?oQa6LKRj211X>eu#0l?0xh==(1uzs?DYdNyDO z=@IN8G7`9mA7pWkcHh|d*bXC7IUQlu~Wr=RXsz?{38qG)jdI>%xL zo*Y764A+WT+yYQcs#=1}*K!33^7Z|<&hOWG-+%FOVqyvLF1gV&tv64I4u-NGZ%P&3 zY|Qt+eRoQ#0BBd2X7@;>4UretZ3q1$A;~?T%{+c_*(ue7oP2$?g@*cj!Ff&7G-N>1 z&+UOhiWQ!{$#puiv8dmh2pAf|H!AhB4i3rstkj&~R#N71WVAuq)0$m8+_q>)TXt_=^PoRqO>X@FYLMz^%8C#Z&BWU!h(u#gf_@+ zBcGS3M+xDFD z-PMw=2KGRe($n9#>VU&`w)%?Fu%@23Lan=g!ETYR<&t7N zYjG%TU;0PzWU$+mFKXl{Stp0QEH-L58|&yEynpr}Pw7zjL}DlZ7mGMo@voK}xPFS> z2YtIkdg;S6!B^K0zbc=d9%r$5s)6QsNJeM)Tv`Hz>n6OuasHQW90AFHtO$Lr*tP4& z4TH^R-($G*<97I1+4{5`MXE1qslyeoiMc0H#bvGGNFKi52j`mfEisC$3Fb?jdwR=? zalZ7O4U4sm);v?Yah_(ZMLO_JD#3jD-uc(_v`(9edda(QIEy@^5fCgPjz#`MYei{a1t25 z0lrC{XHSK$hW_lC_7e219#5a_=~Dfw)zigDmyd73!_y8bA3pi9^-goq+{Lx3oy4tE zbo4BiIeqtVAr_ONlgUdf-MqdO=NP z;UGe@j=r$7^oIolgJP^tKr8Oc8govMDh;%PTQiA1ECXjz=YR5CUyL|5=>FjF$$+NV z*!(Z_@cHx9fUS0Oh5fdDdq(Hkm5@2Sbgdk#9uv&Gocy!+`Hc`RRrz4DaNDP-xs%S$ zzkQo!G3TVItak7a$Z+N4_T`=4fc)7)3yu(-$x_s<4>I%DRF4>l!Vbz%E^)1m*9jio?An4NySYBv{h{9 zDSZI>2X#OGh=+CF_>lZS2;XqY(Odp*D?ZHFLxj8%p}?_v5BaYx*b5tjWz59AS8fLl z1kEij;LlLlXDQ_itl*{X#&FRO>7Jmw|Np20)n((w%b%?7q}V5_jqg`p(@LB5l9bFR z7*lCd!EP8UNeV_N(PQV`Uv=>%cAnMLar|i78~#n1Hm9a*h_YgId1lX@E4k>s=>S8h z=s9vX(!zKQ>z@)m^Po)jw26r2mrEBfrVlS)FDZyEp)aUzH!+Mbl|if;wi zS*SmIu8M5Cr6?fkZuX5jPk*Z!J)V>^<3^-?!_+QAAD|^bdhqQ^HbM1r3Ry798h1I%q58_=Q${7;2I8zkJve94=hcL=Z@FUgh zl4OmQ6EROFecJox;3_1JZcT=N02XB{E^r*yM$tq~pEYF*PpV1MGDGD)>kSHdGGuK$ zDaJy*+PI||X##xZ)@?dDi4>ISaXV;^*FpP45Cs=w~YJ%d>rOj^zsFLDr@d!rbzc_sl=qhC=PJ++YU#SUza zCPs=3FO+MZVuheL0IkB&3ThtUXZx$H8l!i_3(d;4FrT=$IsAv2f!STXniEMLH|YY& zY&NZ{`+zWJwg6BT2$CdrNt{FHQ$cupeQ1VgkXQ5xW9fvR&KgQ}jM18J=HcE?d2usu z#EfoiJf^r#lkBO;s}5A|?k@(tU!?0UsAr~|^f~eI^P3M%TMQVj+VBvglAOltcXYoi z=H$!E_eFX}O^i>LnZiWtK!|-TmM~^6OktvQ0!muFDfF99TE6VdJn%PXw~%73*cnVf zlZnK-aqpvjaM#9#OT4vA6XOO=IM-shOa|hbt)k7!CRsMQ|6pz9G)4xxoNl8z>1?l9 zX>?YyqpKI@rKvPmy4+M6*Ih8Xs05(?4oX6tu-Qyq;E0 zoOn=-#=(_bVcKsbDHUJeQ`Fe|d2b{rHIy4hcMJP?5H{t0NCkI=)7ML^lS@)?S1&jF zOeh}-K;-m`lqpUJtw6gw@ACc&BX?r5iyzvwlU-=j`iBQ;bDn z@wrlopu&J9L{_cn=g;|h*EK`d)2E&q(&+`crkX0}poJwuCmqa-*qLD5-Rj8k3xH+m zxkV|-Q7RF`vbP#8+J}unR@B%nH3Y>?aDsD~LFsETL#n}Fv&8jMtvdStOnz56BCF?> z{$lU3(>eGyVyuiwlK`69Hn0#s?`>pcDuj5QK~1q{F?Zd3(#=AveoRS>B+Rw6>yk=BgImzSa)r=TzmrD?O;up9cO5 z<>9L#P$=l!+)Gtl;}CEus(?!KrAXun?0qk5`7;P$?BMn#`74k<{?gzt(UfZRHSH*U znciI1mQqpEV=u%n!!ri-lmbM;Rj<8~rYh6){(U@xo2Qg0+tXDZOtRfkemhSBW89P9 z6;(X2Y3~6eQ$PhB(OSm8AbhLieLDVTJO}U|rsFnPH!O;8wtF4&w{a;`A*muG{stu- zRDf;qEqpGTg{nj}8v@|jmy-MuA!V2PeBP3Q{02xKBT5|lT9SO@9DO6^1gQ3=m}p`5 za7Qs_lbsfa0TLEDfAgQMD)Y=zA;ZCKiK6L>zi;Tol+9o^U5YOuJRt5qoPO#+}O zFyTYqHfyPmyIX`6puhVY2F%(45Zq_<%tzD-*Q&ftVXx@F^pZS(v0J`?781GnmZ>C^YO<0h`FE?c8_(M`5? za;w&CNLqkKOLo}i);3Q}-)|E_C$~>RNluESvh{}@h%&pq=d~kr+`D#dRzSrZ6 z?mKHT-X~J>1IHW&2qjB%xAO&rj}(rLjCg`iml7;es<4`NC)6^wJ9)py|V;P zUHBIWwuQxB4a@C{(F-!TR# ze0A_7#Uo_Md3c4b1jq?M(imd!bSNGqH3Nu_6%Abt!4^5;XItfZtCJ0V=NXX3`sxFi zJALvPr!hEYqpH0L_J=6?hxQQJ<&5lIPZ7xY-WOrs=pf9pCmhy66GJoZVi42_#q#Qhgy$b4U$RJ&*EU zpg@|1O}qu$w8Hkn9auQmhqolB+V~MYFz_kkz}03f%sNB-RUpe%V-w^z4FJA=^Oua* zhkxogC6Z*3UEr))fF2;{CP&NKR21v?h}LLhK+*&GhVE8tBdHbuua>9i-Hw6w9@aoW zEQp_iO>CQq-K3gPaj>r#9_5mO0KmvO{Y3OdxW;QS0Ev{&#?!25MZC3^H-eRGFPv4%1sa%s=#*Op<9rW#scCol=K^ftkXzY`rko0;`QoHe z1Hb8WKt&-_5@Pl}$w=b^P)rQ=mMe9yaNbbj{qf$@hgIt~ugM1APGPoar>J&zZ7R1h z-nYLCMP*r=Z!&KhizQcxgzaE6z{~N`CKu=FwHmJz{%p5g986vBf+Mn=VAw*{g6G=X zaoBu(NoG!ShFxy5RWG!M0xB_X_Nsal@{BoLW&s=6f`IjCk;2dNG=?yExr)skItdBH z75ULpm^n|slKw|}O0K32hj6TBLxAc!B$S&4LpRZ^po8rP6jn=56a|l~8^_Z7Q}i-= zqTKJFMGfQwkXk-5%5_~k4z-c3rl9NVikotl4Oujad7k?Qj@oF|W6)vZAvoL2m<>bO zfZ3ZGRbTh>s@B`WGD0n*Ec2!L)KPO&{|=eOY9-UV5?@}HGq+qWvvGU&bCU} zDCAj~Xjg9wPg?_oY`Vg&A5w(%Pw&&5l;?`KIM$`MQ;oo(*Pe_NN@+ z+=7R|ll$u+VsbuOWH~o$R`m&D17xXQ&C2uvkKh&Ke-gE2R>pmAK9>)Od}+C*T)rNx z{AXh2ykp3#Uxkz`x?5~37hCl#GcL>0J+qd~j5p-9bNT#Gcbom(z=ylWK(*?2`ah~d zoLoz7g-^(l7Za6h!~JroMgu8V=Asm1!*NMr{FRIw;OZcej>(CG#b9L9HpYnF2b}Nb zUG(bU3miPS+USY&ATq_~QQ~=Iu9E$O4~lnZfQ(8kqqnVA&Z>u1IumWBNs!%b?^SFs zUMnu02&EDs-H~p#m0Jv{1J;1}AZcW@dD=Ar`YAIJ9HcZoz`@xzOBA_dl}ZP5HTF?? zF_vCUIx!F7w1gzk&AR|OIV&jlD;SUxFu$604B232B;3eW0H^8D{cPR>ER=U>lqbP| zQi457Fvt_nbM>1^@Ex)YGK({}BrVL^Se`1hYLw{;)2!Kd1}QP!i732FIbk_lWX^a9 zeu_FLDMDF#;N0LXmCp?Cw9_eGq59??;ql@ ziXTslyG(m`08yts6;hKX;}*gaabuGkXe&dMfd(;8#n}dvp1p{W z^uScJ`J)5Vai2g_bmie*S}DGWm%{?}DNL!d2WXKf?2+P;gmhK^R%^17yXO#HR}QJk zw!&kivM=^bOnuBAJpGF9!W|hmkh!c;$j6~;;A^I3@eD*P>@5b7|1fFUZ15#aacnAx zR7N$%Z_0bj#8AZkV(9TRCaoNDtMGRMP@8NJP0V3K5MMTYK)J<9yHLO(J2a^Nt>e;&lEN}H71WG6721=IA7jq z3p5PnuW*-kp&dEaXQTRDn;Y<@6waI*-a9zlU3e%L!Li1z=L<6uK0Dx<8>(z)ws2t1 zD_yoEkgoJ_tgn;LZ#JrQB8>olVBL;Ca3`h$?2xd(+q*S|z3u@d(6*Z;?U$cVT_x_C zEF^a;F=6h1OtN;wd!TmmZ!|dm8>5l`L!0Km@hSeUU;ZJc;w)N1*#OZD*B81t`m1cR zT}nlXmT5<@3CqLwRf2iD%_Mdr{;%9aD;K)HCOvi=yIjavLIOq)rc^0vI(21cIJ$6o_&7j)%thF>*NBPM&l2X4%Mw`H9~(jxL*~_; zW*<(-K>8Y!y)m94mb6zhbQtCVRb$KB=llt$w}jY4F*EK8MuxaO(ygw=OsB8&^H73p zQ(xpLtidE|uID)l*i>wUk`-aV=hKR7H<=IJ+WbBwkTWsDn2A4v6adc1#5L#Pl%;Hj zqLp_>Nz31~4$krm2*0Gw@FoDM9Ce#sM`2%8%6P!F0Oc4{yWaE@G_bp1FhcK;j6 zo*umW%v=Ti8rpo|>p*Jv>3Iifodd8v0QQg7{0k11pC{=|f$oS5FxSw<=+c@Q~o-!Te^0a%1(xN~K)cmitN`dF7uNq+CKLW!zRbLsyL`Amyw=kB!y;%?71 zyNAZr(6kYj5)Hzquga`aqF4Hos~1`(kbuGE6?9bLb*}YZT}qK{Gg#HRZie`UU@Sb@KGgc9QS^A5+X z?2vaKCd3~i50(Po-gh3{TArPW;d&D5N^?B)3Z_+?}?&@@_1ol10!oIuwnR?X@UBaS*B(l z^?J7+jX|Tw>V9F$#uk

rw?y077{fQGmyL(zl$vx3@x?uJ$b zos}a%xW2xA!5E*bOXE3pT?#rs`9{|-ic9+(K4ah(xQa9zksi&(SH@$HHEfC{DU zOW>5~KCV|PGWI{VYE26@D-i_A^rM#qVIPPYz4*~4C(^@li;$q;Y-WjliDK;~@(tco zukIuQ5xU;EsU`{VkhIBU<|n3=ypHWFv@uh6cxkJHtmKT3+k6{wXRJ-EU`Dc|7#nr` zV8Sk%Bn5w_INN;*tgXO)Ud6K9eA}VgXL{)SV_elFYcBt#sMF(2$=Y)Or7CKbV}2~q zgTbdT$|`0!82Z~MONP2Vu~@3gu*UC*Y5y;0BPGmqd7*ITW}Yc5@_skNiQ0#pu?Vq4 zzLI$gZeFR&vN91Ws?+!%ppXIoM?sly1jF^_^aKO2LEkKphg22!C+0r62UcAcLw&|x z7YH(WfOhX$3UnR1)+bWfiR(+PwEB>;0Woz?F$+@vC5;?AXhwF&uacq@MZeQaL7N4E z%ouwTXU*L$l!*;A?GAh6=v(A_qxAJ)mWD6VqCg`R(%G>2>lZh{-HuG}VFRlhZq|gX zmUpFY`Sp2#@cCDI7tdsm2f(VvD=_yP&j~t6gB^WokeKc;BKH1=MzQH=dXwTel4tS} zYi$L_yfaj*&VVK=ZFT)j$wf}c^SSh&0S~6wz(&yAz(#^_WdkI6zkc$3t(`=IK;Ydq zk7b0{ql{}W+`Gd#7=tCzU=tchx@(Me0!HfOr&WA3s``ND-sJwfk zXmdG<*wx!^Mp{huV&A`0J2addBDb8nD209lgT!Ea}&BLxr z{n8pg>f#dIz;H_qpsWlT0X5|%b%ylX;^X~kjtQkdQhAGGPk3(sPt1F#=)=V|X+zvO zdvW`1%K86&Je38&#{pYt^Y%~JT;$(rWdG?iG5&YK7PV3vI8hS#mbm5L?Y`GFxp2>V zj%dW2ADRhp@D~UZJssr6@Be*BO_>nREw^~tYw=}Cjga>5{I*jh{npfTRxkF>F9roY zZvA_wPIgZ(g9p^UU*zLkAP_hPpc2wj0svidKwm}r(Jm=XCY+2CMUDm3Bf*b={|S~f zUsMO}&?CUrxH^+4(KQ_I;$iLRiHS5bE?ljSw|6Q`{aND9sT#mXa7x+D$ri5b^`()K zAGZ*c8`|1A!JK8=fV5WpZu~^3GQlplH2-$^WMuX|nY@qr`2cr8)^{|2fh_v{0-x57 z_z5T#H>0xk>gC)UY31|$(awDX%9YZ4vw{jsJ#L(N_{#Qt`UsAQ-EBKT8IW^H%TD}r zBuTY-)+Tsfg9r;TF6dd*FpY#3o^#0cuPf{DouY;49M0H_olL@T!)V)&Sz6o8&g%0JOOA(NDl>yw`K8F0{LnCBv4Q zeC_khVv()6DYiCatok#F5={zDk@*i=p3Bd+@Twf#O>2AvTqKqcMv2wWw=S7nXt$>f zGnW8~-ZPta?m>&7!C2%;sYYJy$cJ8^0hnc{v$E7?e5Rg}jLUzl3juOZ6z+6LN;}#C^%i!MmeLJgTY{^547LQ!Wiq64LpiMCiuU zg(j;76{4ET>_LQJn7kb1+?|xs&Z_^IZ|#EI8eWvBy?kKwyluM+ z=*6LH<&BgBUq3}?AJk4>dtnpw5*?M!q^#fbAd@3=5f(6`IVLZkzjgi4r z9hbeao}Z6~0!I4pgXis9ulrQC`XAa?s#^q|D+Rs6Ar*~FT=mtSzZ^Xx+qg$BsJ*G( zE8@TVQ7vbSy9nB&SMNk6Mq;q1-WFj5wZ`Q54*Wuo5T-g&SJozebh;Y4^DO-KmG=kb z`5OF|Mc*dn;5*NpavkGQZ)|T`l>Od@u%)7=PHlQPzsNozIzEyDuRQ!#@P+(eQ|CS% zY^cIs_~AUjA__EB_OvYY- zbavfL-oiA(s#N)P!rRkiW?Pgg!PnyuR$jH8ubo)dWdiC|?M^6}0Ox%qqTu2566il8fke z?1kbSU#|L>v06`qX^XP$GvA9xfM_aEAQD)}f9JX$P0a?dNOax2-_@n+dw19A;UmBS Yor}Hs+Z|uz_UHj$H_<84`u)*=0dRF>&Hw-a literal 0 HcmV?d00001 diff --git a/static/img/getting-started/v3-form-add-to-batch.png b/static/img/getting-started/v3-form-add-to-batch.png new file mode 100644 index 0000000000000000000000000000000000000000..f6cedfac123f2b7eaca7968aab9cfdfd57c15ea4 GIT binary patch literal 19807 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 literal 0 HcmV?d00001 diff --git a/static/img/getting-started/v3-freeze-collection-form.png b/static/img/getting-started/v3-freeze-collection-form.png new file mode 100644 index 0000000000000000000000000000000000000000..1e136b98ee87794ae7c6a13f18c41cf030d62812 GIT binary patch literal 14541 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 S4LpJfc_phNQzm5^^4|cMvmmhm literal 0 HcmV?d00001 diff --git a/static/img/getting-started/v3-mint-token-form.png b/static/img/getting-started/v3-mint-token-form.png new file mode 100644 index 0000000000000000000000000000000000000000..7092917099fc9b3f47b9020b597e4cb0dfa02e84 GIT binary patch literal 23240 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

5#3@5S9bfX0qp+BrnWP*`{+g8Hk!?=7sL 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 B{5Jpq literal 0 HcmV?d00001 diff --git a/static/img/getting-started/v3-set-attribute-form.png b/static/img/getting-started/v3-set-attribute-form.png new file mode 100644 index 0000000000000000000000000000000000000000..56363e0521dc25d07d891fce2471d4c630032238 GIT binary patch literal 20014 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 literal 0 HcmV?d00001 diff --git a/static/img/getting-started/v3-transfer-token-form.png b/static/img/getting-started/v3-transfer-token-form.png new file mode 100644 index 0000000000000000000000000000000000000000..7d1f19b932d61fc623514437483d8d301604ada0 GIT binary patch literal 23398 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 M8=4#7Z#ahi3*z07$^ZZW literal 0 HcmV?d00001 From 9693936b73fdb943e52e8a6952c75de77ee05310 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 18 May 2026 18:15:59 +0300 Subject: [PATCH 22/67] Update WalletConnect guide API calls to v3 GetWallet -> GetAccount, CreateCollection -> CreateTransaction (createCollection action), SendTransaction -> SignTransaction. Add the Bearer auth prefix. Page otherwise kept as-is per its existing "no longer maintained" notice. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../01-using-wallet-connect.md | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md b/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md index c3fbf62..39e6f22 100644 --- a/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md +++ b/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md @@ -129,7 +129,7 @@ yarn start:react-next ``` Now that the project is authenticated with the Enjin Platform, we can go ahead and press the "**Submit Transaction with Platform**" button. -A `CreateCollection` request will be sent to your wallet, and once confirmed, the transaction will be signed and broadcasted to the blockchain. +A collection creation request will be sent to your wallet, and once confirmed, the transaction will be signed and broadcasted to the blockchain. Now it's time to break it down and understand how it works: ### Step #1: Constructing the transaction call @@ -143,23 +143,23 @@ First, we make a query to the user's address to get its nonce: mode: 'no-cors', headers: { 'Content-Type': 'application/json', - 'Authorization': 'my-auth-key', + 'Authorization': 'Bearer my-auth-key', }, body: JSON.stringify({ - query: `query GetWallet($account: String!) { - GetWallet(account: $account) { + query: `query GetAccount($address: String!) { + GetAccount(network: CANARY, chain: MATRIX, address: $address) { nonce } }`, variables: { - account: req.query.address + address: req.query.address } }) }); ``` -Once we get the nonce, we prepare the `CreateCollection` transaction call using Enjin Platform API. -You can use any other mutation Enjin Platform API offers, but for this example we're using CreateCollection. +Next, we prepare the transaction call using the Enjin Platform API. +Every on-chain action is created through the `CreateTransaction` mutation — the action is selected by which field you set on its `transaction` input. For this example we're creating a collection. ```javascript const response = await fetch('https://platform.beta.enjin.io/graphql', { @@ -167,36 +167,37 @@ You can use any other mutation Enjin Platform API offers, but for this example w mode: 'no-cors', headers: { 'Content-Type': 'application/json', - 'Authorization': 'my-auth-key', + 'Authorization': 'Bearer my-auth-key', }, body: JSON.stringify({ - query: `mutation CreateCollection($nonce: Int!, $signingAccount: String!) { - CreateCollection( - mintPolicy: { forceSingleMint: false }, - signingAccount: $signingAccount + query: `mutation CreateTransaction($signerAccount: String!) { + CreateTransaction( + network: CANARY, + chain: MATRIX, + signerAccount: $signerAccount, + transaction: { createCollection: {} } ) { - id - signingPayloadJson(nonce: $nonce) + uuid + encodedData } }`, variables: { - signingAccount: req.query.address, - nonce: nonce + signerAccount: req.query.address } }) }); ``` -We are also passing the `signingAccount`. This is necessary so your daemon doesn't sign the transaction. -In the mutation response, we're asking for the `id` and `signingPayloadJson`, which we will use in the next step. +We are also passing the `signerAccount`. This is necessary so your daemon doesn't sign the transaction. +In the mutation response, we're asking for the `uuid` and `encodedData`, which we will use in the next step. ### Step #2: Sending the Transaction Request to user's wallet With the payload in hand we pass that to our WalletConnect signer and ask it to sign. In the example project, this is done at: `examples/react-next/components/AccountBox.tsx` ```javascript -const txId = data?.data?.CreateCollection?.id; -const payload = data?.data?.CreateCollection?.signingPayloadJson; +const uuid = data?.data?.CreateTransaction?.uuid; +const payload = data?.data?.CreateTransaction?.encodedData; const { signature } = await signer?.signPayload(payload) ``` @@ -204,8 +205,8 @@ The above will trigger the Transaction Request dialog you saw previously, asking ### Step #3: Broadcasting the signed transaction -Finally, we received the `txId`, `signature`, and `payload` from the user's wallet. -We can now broadcast the signed transaction to the blockchain using the Enjin Platform API `SendTransaction` mutation. +Finally, we have the transaction's `uuid` and the signed extrinsic returned by the user's wallet. +We can now submit the signed transaction to the blockchain using the Enjin Platform API `SignTransaction` mutation. This is done at: `examples/react-next/pages/api/send.tsx` ```javascript @@ -214,16 +215,15 @@ This is done at: `examples/react-next/pages/api/send.tsx` mode: 'no-cors', headers: { 'Content-Type': 'application/json', - 'Authorization': 'my-auth-key', + 'Authorization': 'Bearer my-auth-key', }, body: JSON.stringify({ - query: `mutation SendTransaction($id: Int!, $signingPayloadJson: Object!, $signature: String!) { - SendTransaction(id: $id, signingPayloadJson: $signingPayloadJson, signature: $signature) + query: `mutation SignTransaction($uuid: String!, $signedExtrinsic: String!) { + SignTransaction(uuid: $uuid, signedExtrinsic: $signedExtrinsic) }`, variables: { - id: Number.parseInt(req.query.id), - signingPayloadJson: JSON.parse(req.query.signingPayloadJson), - signature: req.query.signature + uuid: req.query.uuid, + signedExtrinsic: req.query.signedExtrinsic } }) }); From 9ad4936e1ee051f080a8fbab0f0c3784f87bc325 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 18 May 2026 18:16:09 +0300 Subject: [PATCH 23/67] Migrate Reading User Wallets guide to v3 API GetWallet -> GetAccount(network, chain, address). Balance is now a single `balance` scalar (no free/reserved split). Tokens come from a flat `tokens` list; collections are read via each token's `collection`. Per-token held balance and the standalone collections query have no v3 equivalent yet, so those are dropped. Pagination tips removed (Account.tokens is unpaginated). SDK tabs kept with the standard not-yet-available warning. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../02-reading-user-wallets.md | 606 ++++-------------- 1 file changed, 121 insertions(+), 485 deletions(-) diff --git a/docs/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md b/docs/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md index babf025..35c0ba0 100644 --- a/docs/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md +++ b/docs/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md @@ -18,18 +18,21 @@ For example, if a user has a particular token in their wallet, they might gain a ::: :::tip Best Practices -- Only pull data from the collections you need. This will improve performance and ensure your backend infrastructure remains scalable. -- Always read and check the balance. Sometimes, the player may appear to hold the token but the balance is actually zero, for instance, if the token has been burned. These burned tokens should not have in-game utility. -- Consider whether you want NFTs that are listed on a marketplace to have utility in your game. Items that are listed for sale don't show up in regular token balance and show up in reserved balance instead. If you want them to have utility, you can check the reserved balance and include the reserved supply. -- When an API call response contains too many results, they are paginated. Users can have hundreds of tokens, in this situation you will need to read them in multiple calls. [Read more about Pagination here](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination). -- If you plan to utilize metadata from on-chain or external sources, it's important to also read the token's attributes. Typically, you'll find a "uri" attribute that points to the external location of this metadata. +- A token appears in a wallet's `tokens` list only while the wallet holds a balance of it, so the list always reflects the wallet's current holdings. +- If you plan to utilize metadata from on-chain or external sources, it's important to also read the token's attributes. Typically, you'll find a `uri` attribute that points to the external location of this metadata. ::: ## Fetching a wallet with the [Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) -### Fetching wallet's Enjin Coin balance +Wallet data is read with the `GetAccount` query. It takes the `network` and `chain` to look up, along with the wallet's `address`. -Use the `GetWallet` query and include the the `balances` in the query response to see how much ENJ a wallet holds. +:::warning SDKs are not yet available +The C# and C++ SDK examples below are out of date and **will not work against the current Enjin Platform API**. This section will be updated once new SDKs are published. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +::: + +### Fetching a wallet's Enjin Coin balance + +Include the `balance` field in the `GetAccount` response to see how much ENJ a wallet holds. **Query:** @@ -37,11 +40,8 @@ Use the `GetWallet` query and include the the `balances` in the query response t ```graphql query FetchWalletBalance{ - GetWallet(account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"){ #Specify the account address - balances{ - free - reserved - } + GetAccount(network: ENJIN, chain: MATRIX, address: "efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr"){ #Specify the network, chain, and account address + balance } } ``` @@ -50,8 +50,8 @@ query FetchWalletBalance{ ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query FetchWalletBalance($account: String!) {\r\n GetWallet(account: $account) {\r\n balances {\r\n free\r\n reserved\r\n }\r\n }\r\n}","variables":{"account":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"}}' +-H 'Authorization: Bearer enjin_api_key' \ +-d '{"query":"query FetchWalletBalance($address: String!) {\r\n GetAccount(network: ENJIN, chain: MATRIX, address: $address) {\r\n balance\r\n }\r\n}","variables":{"address":"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr"}}' ``` @@ -154,20 +154,17 @@ int main() { ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer Your_Platform_Token_Here'}, body: JSON.stringify({ query: ` - query FetchWalletBalance($account: String!) { - GetWallet(account: $account) { - balances{ - free - reserved - } + query FetchWalletBalance($address: String!) { + GetAccount(network: ENJIN, chain: MATRIX, address: $address) { + balance } } `, variables: { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address + address: "efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr" //Specify the account address } }), }) @@ -181,20 +178,17 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - query FetchWalletBalance($account: String!) { - GetWallet(account: $account) { - balances{ - free - reserved - } + query FetchWalletBalance($address: String!) { + GetAccount(network: ENJIN, chain: MATRIX, address: $address) { + balance } } `, variables: { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address + address: "efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr" //Specify the account address } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer Your_Platform_Token_Here'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -205,23 +199,20 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -query FetchWalletBalance($account: String!) { - GetWallet(account: $account) { - balances{ - free - reserved - } +query FetchWalletBalance($address: String!) { + GetAccount(network: ENJIN, chain: MATRIX, address: $address) { + balance } } ''' variables = { - 'account': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the account address + 'address': "efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr" #Specify the account address } response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer Your_Platform_Token_Here'} ) print(response.json()) ``` @@ -233,44 +224,39 @@ print(response.json()) ```json { "data": { - "GetWallet": { - "balances": { - "free": "86010842630734264894", //~86.01084... ENJ - "reserved": "13900475000000000000" //~13.90047... ENJ - } + "GetAccount": { + "balance": "11799982990638599996" //~11.79998... ENJ } } } ``` :::info **Formatting Balances** -The API returns balance fields in the **base unit** (u128), meaning they appear as large integers without decimals. To obtain the readable ENJ amount, **divide the returned value by 10^18**. +The API returns the `balance` field in the **base unit** (u128), meaning it appears as a large integer without decimals. To obtain the readable ENJ amount, **divide the returned value by 10^18**. - **Formula:** `Base Unit Value` / `1,000,000,000,000,000,000` = `ENJ Amount` -- **Example:** A value of `86010842630734264894` converts to approximately **86.01 ENJ**. +- **Example:** A value of `11799982990638599996` converts to approximately **11.80 ENJ**. ::: -### Fetch a wallet's collections +### Fetching the tokens a wallet holds -Use the `GetWallet` query and include the the `collectionAccounts` in the query response to see what collections a wallet holds. +Include the `tokens` field in the `GetAccount` response to list every token the wallet currently holds. Each token also carries its parent `collection`, so this single query tells you which collections the wallet holds items in as well. **Query:** ```graphql -query FetchingWalletCollections{ - GetWallet(account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"){ #Specify the account address - collectionAccounts{ - edges{ - node{ - collection{ - collectionId - attributes{ - key - value - } - } - } +query FetchWalletTokens{ + GetAccount(network: ENJIN, chain: MATRIX, address: "efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr"){ #Specify the network, chain, and account address + tokens{ + id + tokenId + collection{ + id + } + attributes{ + key + value } } } @@ -281,324 +267,8 @@ query FetchingWalletCollections{ ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query FetchingWalletCollections($account: String!) {\r\n GetWallet(account: $account) {\r\n collectionAccounts {\r\n edges {\r\n node {\r\n collection {\r\n collectionId\r\n attributes {\r\n key\r\n value\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}","variables":{"account":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the query -var getWallet = new GetWallet() - .SetAccount("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"); - -// Define and assign the return data fragment to the query -var walletFragment = new WalletFragment() - .WithCollectionAccounts(new ConnectionFragment() - .WithEdges(new EdgeFragment() - .WithNode(new CollectionAccountFragment() - .WithCollection(new CollectionFragment() - .WithCollectionId() - .WithAttributes(new AttributeFragment() - .WithKey() - .WithValue() - ) - ) - ) - ) - ); - -getWallet.Fragment(walletFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.beta.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendGetWallet(getWallet); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreQueries.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Set up the query - GetWallet getWallet = GetWallet() - .SetAccount(make_shared("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f")); - - // Define and assign the return data fragments to the query - AccountFragment accountFragment = AccountFragment() - .WithAddress(); - WalletFragment walletFragment = WalletFragment() - .WithAccount(make_shared(accountFragment)); - AttributeFragment attributeFragment = AttributeFragment() - .WithKey() - .WithValue(); - CollectionFragment collectionFragment = CollectionFragment() - .WithCollectionId() - .WithAttributes(make_shared(attributeFragment)); - CollectionAccountFragment collectionAccountFragment = CollectionAccountFragment() - .WithCollection(make_shared(collectionFragment)); - EdgeFragment edgeFragment = EdgeFragment() - .WithNode(make_shared(collectionAccountFragment)); - - ConnectionFragment connectionFragment = ConnectionFragment() - .WithEdges(make_shared>(edgeFragment)); - - walletFragment.WithCollectionAccounts(make_shared>(connectionFragment)); - - getWallet.SetFragment(make_shared(walletFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.beta.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendGetWallet(*client, getWallet); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& getWalletResult = gqlResult->GetData()->GetResult(); - - std::cout << getWalletResult->GetAccount()->GetAddress().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.beta.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query FetchingWalletCollections($account: String!) { - GetWallet(account: $account) { - collectionAccounts{ - edges{ - node{ - collection{ - collectionId - attributes{ - key - value - } - } - } - } - } - } - } - `, - variables: { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.beta.enjin.io/graphql', { - query: ` - query FetchingWalletCollections($account: String!) { - GetWallet(account: $account) { - collectionAccounts{ - edges{ - node{ - collection{ - collectionId - attributes{ - key - value - } - } - } - } - } - } - } - `, - variables: { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -query FetchingWalletCollections($account: String!) { - GetWallet(account: $account) { - collectionAccounts{ - edges{ - node{ - collection{ - collectionId - attributes{ - key - value - } - } - } - } - } - } -} -''' - -variables = { - 'account': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the account address -} - -response = requests.post('https://platform.beta.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -**Response:** - -```json -{ - "data": { - "GetWallet": { - "collectionAccounts": { - "edges": [ - { - "node": { - "collection": { - "collectionId": "33866", - "attributes": [] - } - } - }, - { - "node": { - "collection": { - "collectionId": "36105", - "attributes": [ - { - "key": "name", - "value": "My test collection" - } - ] - } - } - }, - { - "node": { - "collection": { - "collectionId": "36623", - "attributes": [ - { - "key": "name", - "value": "Docs Testing Collection" - } - ] - } - } - } - ] - } - } - } -} -``` - -:::tip Using Pagination -The response may be displayed on several pages. To view all of it, you may need to follow steps for [pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination) which allows you to flip through the pages. -::: - -### Fetch a wallet's tokens - -Use the `GetWallet` query and include the the `tokenAccounts` in the query response to see what tokens a wallet holds. - -**Query:** - - - -```graphql -query FetchingWalletTokens{ - GetWallet(account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"){ #Specify the account address - tokenAccounts{ - edges{ - node{ - balance - token{ - tokenId - collection{ - collectionId - } - attributes{ - key - value - } - } - } - } - } - } -} -``` - - -``` -curl -X POST "https://platform.beta.enjin.io/graphql" \ --H "Content-Type: application/json" \ --H "Authorization: enjin_api_key" \ --d '{"query": "query FetchingWalletTokens { GetWallet(account: \"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f\") { tokenAccounts { edges { node { balance token { tokenId collection { collectionId } attributes { key value } } } } } } }"}' +-H 'Authorization: Bearer enjin_api_key' \ +-d '{"query":"query FetchWalletTokens($address: String!) {\r\n GetAccount(network: ENJIN, chain: MATRIX, address: $address) {\r\n tokens {\r\n id\r\n tokenId\r\n collection {\r\n id\r\n }\r\n attributes {\r\n key\r\n value\r\n }\r\n }\r\n }\r\n}","variables":{"address":"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr"}}' ``` @@ -732,33 +402,27 @@ int main() { ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer Your_Platform_Token_Here'}, body: JSON.stringify({ query: ` - query FetchingWalletTokens($account: String!) { - GetWallet(account: $account) { - tokenAccounts{ - edges{ - node{ - balance - token{ - tokenId - collection{ - collectionId - } - attributes{ - key - value - } - } - } + query FetchWalletTokens($address: String!) { + GetAccount(network: ENJIN, chain: MATRIX, address: $address) { + tokens{ + id + tokenId + collection{ + id + } + attributes{ + key + value } } } } `, variables: { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address + address: "efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr" //Specify the account address } }), }) @@ -772,33 +436,27 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - query FetchingWalletTokens($account: String!) { - GetWallet(account: $account) { - tokenAccounts{ - edges{ - node{ - balance - token{ - tokenId - collection{ - collectionId - } - attributes{ - key - value - } - } - } + query FetchWalletTokens($address: String!) { + GetAccount(network: ENJIN, chain: MATRIX, address: $address) { + tokens{ + id + tokenId + collection{ + id + } + attributes{ + key + value } } } } `, variables: { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address + address: "efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr" //Specify the account address } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer Your_Platform_Token_Here'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -809,23 +467,17 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -query FetchingWalletTokens($account: String!) { - GetWallet(account: $account) { - tokenAccounts{ - edges{ - node{ - balance - token{ - tokenId - collection{ - collectionId - } - attributes{ - key - value - } - } - } +query FetchWalletTokens($address: String!) { + GetAccount(network: ENJIN, chain: MATRIX, address: $address) { + tokens{ + id + tokenId + collection{ + id + } + attributes{ + key + value } } } @@ -833,12 +485,12 @@ query FetchingWalletTokens($account: String!) { ''' variables = { - 'account': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the account address + 'address': "efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr" #Specify the account address } response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer Your_Platform_Token_Here'} ) print(response.json()) ``` @@ -850,65 +502,49 @@ print(response.json()) ```json { "data": { - "GetWallet": { - "tokenAccounts": { - "edges": [ - { - "node": { - "balance": "1", - "token": { - "tokenId": "0", - "collection": { - "collectionId": "33866" - }, - "attributes": [] - } - } + "GetAccount": { + "tokens": [ + { + "id": "2100-23", + "tokenId": "23", + "collection": { + "id": "2100" }, - { - "node": { - "balance": "1", - "token": { - "tokenId": "0", - "collection": { - "collectionId": "36105" - }, - "attributes": [ - { - "key": "Name", - "value": "Awesome Token!" - } - ] - } + "attributes": [ + { + "key": "uri", + "value": "https://etherscapegame.com/crypto/Detritus.json?ver=2" } + ] + }, + { + "id": "4265-1", + "tokenId": "1", + "collection": { + "id": "4265" }, - { - "node": { - "balance": "6", - "token": { - "tokenId": "1", - "collection": { - "collectionId": "36105" - }, - "attributes": [ - { - "key": "name", - "value": "Awesome Token 2!" - } - ] - } + "attributes": [ + { + "key": "name", + "value": "test 1" } - } - ] - } + ] + }, + { + "id": "4001-3", + "tokenId": "3", + "collection": { + "id": "4001" + }, + "attributes": [] + } + ] } } } ``` -:::tip Using Pagination -The response may be displayed on several pages. To view all of it, you may need to follow steps for [pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination) which allows you to flip through the pages. -::: +Each token's canonical `id` (for example `2100-23`) combines its `collection` id and `tokenId`. :::info Explore More Arguments For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. From 28eee4d31434ee8c3ad2e534d57a7c842e531cfa Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 18 May 2026 19:01:46 +0300 Subject: [PATCH 24/67] Migrate Using Managed Wallets guide to v3 API CreateWallet -> CreateManagedWallet; GetWallet -> GetManagedWallet (returns publicKey + externalId). Mint/transfer examples go through CreateTransaction with the mintToken / transferToken actions. Restructure: the "any transaction can be signed by a managed wallet" capability is lifted out of an inline callout into a dedicated "Signing transactions as a Managed Wallet" section that teaches the signerAccount / signerExternalId arguments before the examples. SDK tabs kept verbatim with the standard not-yet-available warning. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../03-using-managed-wallets.md | 422 ++++++++---------- 1 file changed, 193 insertions(+), 229 deletions(-) diff --git a/docs/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md b/docs/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md index 37bc5c6..2e7da17 100644 --- a/docs/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md +++ b/docs/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md @@ -14,9 +14,9 @@ This approach offers transparency, allowing users to easily track their digital Here's how the typical flow works: -1. **[Managed Wallet Creation](#create-managed-wallets):** When a new user starts using your application, you initiate the creation of a managed wallet for them, and assign the user's internal ID from your database to the newly created managed wallet. This ensures a consistent link between the user and their wallet. -2. **[Asset Management](#minting-tokens-to-managed-wallets):** As the user earns or deposits digital assets within your application, these assets are automatically stored in their managed wallet. -3. **[In-App Transactions](#minting-tokens-to-managed-wallets):** Any actions the user performs within your application that involve their wallet (e.g., spending tokens, participating in in-game economies) are executed directly on their managed wallet. +1. **[Managed Wallet Creation](#creating-a-managed-wallet):** When a new user starts using your application, you initiate the creation of a managed wallet for them, and assign the user's internal ID from your database to the newly created managed wallet. This ensures a consistent link between the user and their wallet. +2. **[Asset Management](#minting-tokens-to-a-managed-wallet):** As the user earns or deposits digital assets within your application, these assets are automatically stored in their managed wallet. +3. **[In-App Transactions](#signing-transactions):** Any actions the user performs within your application that involve their wallet (e.g., spending tokens, participating in in-game economies) are executed directly on their managed wallet. 4. **[User Withdrawal (Optional)](#transferring-tokens-from-managed-wallets):** If a user decides they want to take full control of their assets, you can facilitate the transfer of all funds from their managed wallet to their own self-custodial wallet. This process ensures that users can interact with blockchain assets seamlessly within your application without needing to understand the underlying complexities of blockchain technology or operating his own crypto wallet. @@ -30,9 +30,13 @@ You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://fa --- -## Create Managed Wallets +:::warning SDKs are not yet available +The C# and C++ SDK examples below are out of date and **will not work against the current Enjin Platform API**. This section will be updated once new SDKs are published. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +::: + +## Creating a Managed Wallet {#creating-a-managed-wallet} -To create a Managed wallet, run the `CreateWallet` mutation, with a unique ID as a parameter. +To create a managed wallet, run the `CreateManagedWallet` mutation with a unique ID as a parameter. Choose a unique `externalId` for each player/user that can be cross-referenced later. This unique identifier should be something already associated with the player in your database, such as a player ID or username. By doing so, you will be able to consistently link the Managed Wallet to the respective player. @@ -42,7 +46,7 @@ By doing so, you will be able to consistently link the Managed Wallet to the res ```graphql mutation CreateManagedWallet { - CreateWallet(externalId: "player_1_id") #Replace this with a unique ID + CreateManagedWallet(externalId: "docs-example-player") #Replace this with a unique ID } ``` @@ -50,8 +54,8 @@ mutation CreateManagedWallet { ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation CreateManagedWallet($external_id: String!) {\r\n CreateWallet(externalId: $external_id)\r\n}","variables":{"externalId":"player_1_id"}}' +-H 'Authorization: Bearer enjin_api_key' \ +-d '{"query":"mutation CreateManagedWallet($externalId: String!) {\r\n CreateManagedWallet(externalId: $externalId)\r\n}","variables":{"externalId":"docs-example-player"}}' ``` @@ -138,12 +142,12 @@ fetch('https://platform.beta.enjin.io/graphql', { headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ query: ` - mutation CreateManagedWallet($external_id: String!) { - CreateWallet(externalId: $external_id) + mutation CreateManagedWallet($externalId: String!) { + CreateManagedWallet(externalId: $externalId) } `, variables: { - externalId: "player_1_id" //Replace this with a unique ID + externalId: "docs-example-player" //Replace this with a unique ID } }), }) @@ -157,15 +161,15 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation CreateManagedWallet($external_id: String!) { - CreateWallet(externalId: $external_id) + mutation CreateManagedWallet($externalId: String!) { + CreateManagedWallet(externalId: $externalId) } `, variables: { - externalId: "player_1_id" //Replace this with a unique ID + externalId: "docs-example-player" //Replace this with a unique ID } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer Your_Platform_Token_Here'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -176,18 +180,18 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation CreateManagedWallet($external_id: String!) { - CreateWallet(externalId: $external_id) +mutation CreateManagedWallet($externalId: String!) { + CreateManagedWallet(externalId: $externalId) } ''' variables = { - 'externalId': "player_1_id" #Replace this with a unique ID + 'externalId': "docs-example-player" #Replace this with a unique ID } response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer Your_Platform_Token_Here'} ) print(response.json()) ``` @@ -195,29 +199,25 @@ print(response.json()) :::warning Lost database data? -Recreate the Managed wallets by running `CreateWallet` mutation again for each of the `externalId`s. -Make sure to use the same Daemon wallet seed and password used to create Managed wallets prior, as Managed wallets are derived with the following derivation path: `walletSeed/externalId///password` +Recreate the managed wallets by running the `CreateManagedWallet` mutation again for each of the `externalId`s. +Make sure to use the same Daemon wallet seed and password used to create the managed wallets prior, as managed wallets are derived with the following derivation path: `walletSeed/externalId///password` ::: -## Interact with Managed Wallets +## Finding a Managed Wallet {#finding-a-managed-wallet} -Once the Managed wallet is created, you can provide the `externalId` field of the Wallet object to any query or mutation that accepts a `Wallet.externalId` parameter, in order to use a Managed wallet instead of any other wallet. +Once a managed wallet is created, use the `GetManagedWallet` query to look it up by its `externalId` and retrieve its on-chain `publicKey`. -Lets look at the `GetWallet` query as an example, to [get the Public Key of an account](/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md), we use the account's address. `account: "address_here"` - -However, to get the Public Key of a `Managed wallet`, we provide the `Wallet.externalId` parameter instead. +You'll need this public key to mint tokens to the wallet, and it can also be used to identify the wallet when signing transactions on its behalf. **Query:** ```graphql -query GetManagedWalletPublicKey { - GetWallet(externalId: "player_1_id") { #Specify the managed wallet unique ID - account { - address - publicKey - } +query GetManagedWallet { + GetManagedWallet(network: CANARY, chain: MATRIX, externalId: "docs-example-player") { #Specify the managed wallet's unique ID + publicKey + externalId } } ``` @@ -226,8 +226,8 @@ query GetManagedWalletPublicKey { ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query GetManagedWalletPublicKey($external_id: String!) {\r\n GetWallet(externalId: $external_id) {\r\n account {\r\n address\r\n publicKey\r\n }\r\n }\r\n}","variables":{"externalId":"player_1_id"}}' +-H 'Authorization: Bearer enjin_api_key' \ +-d '{"query":"query GetManagedWallet($externalId: String!) {\r\n GetManagedWallet(network: CANARY, chain: MATRIX, externalId: $externalId) {\r\n publicKey\r\n externalId\r\n }\r\n}","variables":{"externalId":"docs-example-player"}}' ``` @@ -330,17 +330,15 @@ fetch('https://platform.beta.enjin.io/graphql', { headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ query: ` - query GetManagedWalletPublicKey($external_id: String!) { - GetWallet(externalId: $external_id) { - account { - address - publicKey - } + query GetManagedWallet($externalId: String!) { + GetManagedWallet(network: CANARY, chain: MATRIX, externalId: $externalId) { + publicKey + externalId } } `, variables: { - externalId: "player_1_id" //Specify the managed wallet unique ID + externalId: "docs-example-player" //Specify the managed wallet's unique ID } }), }) @@ -354,20 +352,18 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - query GetManagedWalletPublicKey($external_id: String!) { - GetWallet(externalId: $external_id) { - account { - address - publicKey - } + query GetManagedWallet($externalId: String!) { + GetManagedWallet(network: CANARY, chain: MATRIX, externalId: $externalId) { + publicKey + externalId } } `, variables: { - externalId: "player_1_id" //Specify the managed wallet unique ID + externalId: "docs-example-player" //Specify the managed wallet's unique ID } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer Your_Platform_Token_Here'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -378,23 +374,21 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -query GetManagedWalletPublicKey($external_id: String!) { - GetWallet(externalId: $external_id) { - account { - address - publicKey - } +query GetManagedWallet($externalId: String!) { + GetManagedWallet(network: CANARY, chain: MATRIX, externalId: $externalId) { + publicKey + externalId } } ''' variables = { - 'externalId': "player_1_id" #Specify the managed wallet unique ID + 'externalId': "docs-example-player" #Specify the managed wallet's unique ID } response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer Your_Platform_Token_Here'} ) print(response.json()) ``` @@ -406,38 +400,36 @@ print(response.json()) ```json { "data": { - "GetWallet": { - "account": { - "address": "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT", //Account address for Managed wallet ID "player_1_id" - "publicKey": "0x92f33d3efd6af37798b125cba08e21fc7b404293f12c067f1cb6ab326775ff08" - } + "GetManagedWallet": { + "publicKey": "0xded3c8f0296f5ee023f07aa5617fc261bd5991c4474ee775a16ec35c1d1a1e3a", + "externalId": "docs-example-player" } } } ``` -## Minting tokens to Managed wallets +## Minting tokens to a Managed Wallet {#minting-tokens-to-a-managed-wallet} -With the acquired Managed wallet address, you can mint tokens directly to the Managed wallet. +With the managed wallet's public key in hand, you can mint tokens directly to it by setting it as the `recipient`. ```graphql -mutation BatchMint { - BatchMint( - collectionId: 7154 #Specify the collection ID - recipients: [ - { - account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" #The recipient of the mint (the Managed wallet account address from the GetWallet query) - mintParams: { - amount:1 #Amount to mint - tokenId: {integer: 6533} #Token ID to mint - } - } - ] +mutation MintToManagedWallet { + CreateTransaction( + network: CANARY + chain: MATRIX + transaction: { + mintToken: { + recipient: "0xded3c8f0296f5ee023f07aa5617fc261bd5991c4474ee775a16ec35c1d1a1e3a" #The managed wallet's public key, from the GetManagedWallet query + collectionId: 36105 #Specify the collection ID + tokenId: 1 #Token ID to mint + amount: 1 #Amount to mint + } + } ) { - id - method + uuid + action state } } @@ -447,8 +439,8 @@ mutation BatchMint { ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation BatchMint($collection_id: BigInt!) {\r\n BatchMint(\r\n collectionId: $collection_id\r\n recipients: [\r\n {\r\n account: \"cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT\" #The recipient of the mint (the Managed wallet account address from the GetWallet query)\r\n mintParams: {\r\n amount: 1 #Amount to mint\r\n tokenId: { integer: 6533 } #Token ID to mint\r\n }\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":7154}}' +-H 'Authorization: Bearer enjin_api_key' \ +-d '{"query":"mutation MintToManagedWallet {\r\n CreateTransaction(\r\n network: CANARY\r\n chain: MATRIX\r\n transaction: {\r\n mintToken: {\r\n recipient: \"0xded3c8f0296f5ee023f07aa5617fc261bd5991c4474ee775a16ec35c1d1a1e3a\"\r\n collectionId: 36105\r\n tokenId: 1\r\n amount: 1\r\n }\r\n }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{}}' ``` @@ -579,28 +571,25 @@ fetch('https://platform.beta.enjin.io/graphql', { headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ query: ` - mutation BatchMint($collection_id: BigInt!) { - BatchMint( - collectionId: $collection_id - recipients: [ - { - account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" #The recipient of the mint (the Managed wallet account address from the GetWallet query) - mintParams: { - amount:1 #Amount to mint - tokenId: {integer: 6533} #Token ID to mint - } + mutation MintToManagedWallet { + CreateTransaction( + network: CANARY + chain: MATRIX + transaction: { + mintToken: { + recipient: "0xded3c8f0296f5ee023f07aa5617fc261bd5991c4474ee775a16ec35c1d1a1e3a" #The managed wallet's public key, from the GetManagedWallet query + collectionId: 36105 #Specify the collection ID + tokenId: 1 #Token ID to mint + amount: 1 #Amount to mint } - ] + } ) { - id - method + uuid + action state } } - `, - variables: { - collection_id: 7154 //Specify the managed wallet unique ID - } + ` }), }) .then(response => response.json()) @@ -613,30 +602,27 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation BatchMint($collection_id: BigInt!) { - BatchMint( - collectionId: $collection_id - recipients: [ - { - account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" #The recipient of the mint (the Managed wallet account address from the GetWallet query) - mintParams: { - amount:1 #Amount to mint - tokenId: {integer: 6533} #Token ID to mint - } + mutation MintToManagedWallet { + CreateTransaction( + network: CANARY + chain: MATRIX + transaction: { + mintToken: { + recipient: "0xded3c8f0296f5ee023f07aa5617fc261bd5991c4474ee775a16ec35c1d1a1e3a" #The managed wallet's public key, from the GetManagedWallet query + collectionId: 36105 #Specify the collection ID + tokenId: 1 #Token ID to mint + amount: 1 #Amount to mint } - ] + } ) { - id - method + uuid + action state } } - `, - variables: { - collection_id: 7154 //Specify the managed wallet unique ID - } + ` }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer Your_Platform_Token_Here'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -647,66 +633,69 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation BatchMint($collection_id: BigInt!) { - BatchMint( - collectionId: $collection_id - recipients: [ - { - account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" #The recipient of the mint (the Managed wallet account address from the GetWallet query) - mintParams: { - amount:1 #Amount to mint - tokenId: {integer: 6533} #Token ID to mint - } - } - ] +mutation MintToManagedWallet { + CreateTransaction( + network: CANARY + chain: MATRIX + transaction: { + mintToken: { + recipient: "0xded3c8f0296f5ee023f07aa5617fc261bd5991c4474ee775a16ec35c1d1a1e3a" + collectionId: 36105 + tokenId: 1 + amount: 1 + } + } ) { - id - method + uuid + action state } } ''' -variables = { - 'collection_id': 7154 #Specify the managed wallet unique ID -} - response = requests.post('https://platform.beta.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + json={'query': query}, + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer Your_Platform_Token_Here'} ) print(response.json()) ``` -## Transferring tokens from managed wallets +## Signing transactions as a Managed Wallet {#signing-transactions} -If you followed along the previous snippets of code, you should have a Managed wallet with a token in it. To transfer it out to another wallet, we can use the `BatchTransfer` mutation. +By default, every transaction the Enjin Platform creates is signed by your [Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md). To act on a user's behalf instead, you can have the platform sign a transaction with that user's managed wallet. -:::info Signing transactions using a managed wallet -It's important to note that every mutation that accepts a `signingAccount` argument can be used to sign the transaction using a managed wallet, as shown in the batch transfer tutorial below. -::: +Both `CreateTransaction` and `CreateBatchTransaction` accept two optional arguments for this: + +- **`signerExternalId`** — the managed wallet's `externalId`. This is the most direct option, as you don't need to look the wallet's public key up first. +- **`signerAccount`** — the managed wallet's public key, as returned by [`GetManagedWallet`](#finding-a-managed-wallet). + +Set either one, and the platform signs that transaction with the corresponding managed wallet instead of the Wallet Daemon. Because these arguments live on `CreateTransaction` itself, **any** on-chain action — a transfer, a listing, an attribute change, anything you set on the `transaction` input — can be performed as a managed wallet. + +## Transferring tokens from a Managed Wallet {#transferring-tokens-from-managed-wallets} + +If you followed along with the previous snippets of code, you should have a managed wallet with a token in it. To transfer it out to another wallet, set `signerExternalId` to the managed wallet and use the `transferToken` action. ```graphql -mutation BatchTransfer { - BatchTransfer( - collectionId: 7154 #Specify the collection ID - signingAccount: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" # Add your signing account address (the Managed wallet account address from the GetWallet query) - recipients: [ - { - account: "cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ" #The recipient of the transfer - simpleParams: { - tokenId: { integer: 6533 } #Token ID to transfer - amount: 1 #Amount to transfer - } +mutation TransferFromManagedWallet { + CreateTransaction( + network: CANARY + chain: MATRIX + signerExternalId: "docs-example-player" #The managed wallet that will sign this transaction + transaction: { + transferToken: { + recipient: "cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ" #The recipient of the transfer + collectionId: 36105 #Specify the collection ID + tokenId: 1 #Token ID to transfer + amount: 1 #Amount to transfer } - ] + } ) { - id - method + uuid + action state } } @@ -716,8 +705,8 @@ mutation BatchTransfer { ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation BatchTransfer($collection_id: BigInt!, $signing_account: String!) {\r\n BatchTransfer(\r\n collectionId: $collection_id\r\n signingAccount: $signing_account\r\n recipients: [\r\n {\r\n account: \"cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ\" #The recipient of the transfer\r\n simpleParams: {\r\n tokenId: { integer: 6533 } #Token ID to transfer\r\n amount: 1 #Amount to transfer\r\n }\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":7154,"signing_account":"cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT"}}' +-H 'Authorization: Bearer enjin_api_key' \ +-d '{"query":"mutation TransferFromManagedWallet {\r\n CreateTransaction(\r\n network: CANARY\r\n chain: MATRIX\r\n signerExternalId: \"docs-example-player\"\r\n transaction: {\r\n transferToken: {\r\n recipient: \"cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ\"\r\n collectionId: 36105\r\n tokenId: 1\r\n amount: 1\r\n }\r\n }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{}}' ``` @@ -849,34 +838,26 @@ fetch('https://platform.beta.enjin.io/graphql', { headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, body: JSON.stringify({ query: ` - mutation BatchTransfer - ( - $collection_id: BigInt! - $signing_account: String! - ) { - BatchTransfer( - collectionId: $collection_id - signingAccount: $signing_account - recipients: [ - { - account: "cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ" #The recipient of the transfer - simpleParams: { - tokenId: { integer: 6533 } #Token ID to transfer - amount: 1 #Amount to transfer - } + mutation TransferFromManagedWallet { + CreateTransaction( + network: CANARY + chain: MATRIX + signerExternalId: "docs-example-player" #The managed wallet that will sign this transaction + transaction: { + transferToken: { + recipient: "cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ" #The recipient of the transfer + collectionId: 36105 #Specify the collection ID + tokenId: 1 #Token ID to transfer + amount: 1 #Amount to transfer } - ] + } ) { - id - method + uuid + action state } } - `, - variables: { - collection_id: 7154, //Specify the collection ID - signing_account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" //Add your signing account address (the Managed wallet account address from the GetWallet query) - } + ` }), }) .then(response => response.json()) @@ -889,36 +870,28 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation BatchTransfer - ( - $collection_id: BigInt! - $signing_account: String! - ) { - BatchTransfer( - collectionId: $collection_id - signingAccount: $signing_account - recipients: [ - { - account: "cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ" #The recipient of the transfer - simpleParams: { - tokenId: { integer: 6533 } #Token ID to transfer - amount: 1 #Amount to transfer - } + mutation TransferFromManagedWallet { + CreateTransaction( + network: CANARY + chain: MATRIX + signerExternalId: "docs-example-player" #The managed wallet that will sign this transaction + transaction: { + transferToken: { + recipient: "cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ" #The recipient of the transfer + collectionId: 36105 #Specify the collection ID + tokenId: 1 #Token ID to transfer + amount: 1 #Amount to transfer } - ] + } ) { - id - method + uuid + action state } } - `, - variables: { - collection_id: 7154, //Specify the collection ID - signing_account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" //Add your signing account address (the Managed wallet account address from the GetWallet query) - } + ` }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer Your_Platform_Token_Here'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -929,46 +902,37 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation BatchTransfer -( - $collection_id: BigInt! - $signing_account: String! -) { - BatchTransfer( - collectionId: $collection_id - signingAccount: $signing_account - recipients: [ - { - account: "cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ" #The recipient of the transfer - simpleParams: { - tokenId: { integer: 6533 } #Token ID to transfer - amount: 1 #Amount to transfer - } +mutation TransferFromManagedWallet { + CreateTransaction( + network: CANARY + chain: MATRIX + signerExternalId: "docs-example-player" + transaction: { + transferToken: { + recipient: "cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ" + collectionId: 36105 + tokenId: 1 + amount: 1 } - ] + } ) { - id - method + uuid + action state } } ''' -variables = { - 'collection_id': 7154, #Specify the collection ID - 'signing_account': "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" #Add your signing account address (the Managed wallet account address from the GetWallet query) -} - response = requests.post('https://platform.beta.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + json={'query': query}, + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer Your_Platform_Token_Here'} ) print(response.json()) ``` -Make sure that `signingAccount` is set to the Managed Wallet address that owns that token. +Make sure `signerExternalId` (or `signerAccount`) is set to the managed wallet that holds the token being transferred. :::info Explore More Arguments For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. From 662d2b83bc0a355f456fe0a8d2844c8fba7b1073 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Sun, 24 May 2026 15:34:12 +0300 Subject: [PATCH 25/67] Migrate Destroying a Collection section to v3 API Rewrite the destroy-collection examples to use the new `destroyCollection` discriminator on `CreateTransaction` (input shape: `{ id: BigInt! }`). Remove the "Section under review" callouts now that the v3 mutation is available, and update the dashboard signing notes to the v3 Signing Account framing. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../08-burning-destroying-tokens.md | 107 +++++++++--------- 1 file changed, 56 insertions(+), 51 deletions(-) diff --git a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md index 6266e69..b35b75c 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md @@ -59,10 +59,6 @@ To destroy a token, follow the above instructions for Melting a token, but make ### Destroying a collection -:::warning Section under review -The dashboard flow for destroying a collection is being verified — the GraphQL API currently exposes no `destroyCollection` action, so the steps below may not match the current platform. We'll refresh this section once verified. -::: - :::info To destroy a collection, these requirements must be met: - The caller is the collection owner - The collection has no attributes @@ -71,17 +67,16 @@ The dashboard flow for destroying a collection is being verified — the GraphQL - If the collection has some tokens, you can follow the above guide [Destroying a token and removing it from the Blockchain](#destroying-a-token-and-removing-it-from-the-blockchain) for each of the tokens in the collection, to destroy them all. ::: -In the Platform menu, navigate to "**[Collections](https://platform.beta.enjin.io/collections)**". -**Locate the collection** you wish to destroy, click the **3 vertical dots** (**⋮**) on its row, then click the "**Destroy**" button. +In the Platform menu, navigate to "**[Collections](https://platform.beta.enjin.io/collections)**", **locate the collection** you wish to destroy, click the **3 vertical dots** (**⋮**) on its row, then click "**Destroy**". -Then, confirm by clicking the "**Destroy**" button +Confirm by clicking the "**Destroy**" button. -The Transaction Request will then appear in the "**Transactions**" menu +A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. -Since this request requires a , it'll need to be signed with your Wallet. +Since this request requires a , it must be signed before it broadcasts. -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. +- By default, transactions are signed automatically by the **Wallet Daemon**. +- To sign with a different account, expand **Transaction Options → Signing Account** on the form and provide a [Managed Wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) address. ## Option B. Using the Enjin API & SDKs @@ -460,27 +455,31 @@ Once the transaction is executed, the token will be destroyed and the ```graphql mutation DestroyCollection { - DestroyCollection( - collectionId: 68844 #Specify the Collection ID + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + destroyCollection: { + id: 68844 + } + } ) { - id - method + uuid + action state } } @@ -490,8 +489,8 @@ mutation DestroyCollection { ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation DestroyCollection($collection_id: BigInt!) {\r\n DestroyCollection(collectionId: $collection_id) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation DestroyCollection($id: BigInt!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: { destroyCollection: { id: $id } }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"id":68844}}' ``` @@ -533,22 +532,24 @@ Snippet In Progress ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` - mutation DestroyCollection($collection_id: BigInt!){ - DestroyCollection( - collectionId: $collection_id + mutation DestroyCollection($id: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + destroyCollection: { id: $id } + } ) { - id - method + uuid + action state } } `, - variables: { - collection_id: 36105 //Specify the collection ID - } + variables: { id: 68844 } }), }) .then(response => response.json()) @@ -561,21 +562,23 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation DestroyCollection($collection_id: BigInt!){ - DestroyCollection( - collectionId: $collection_id + mutation DestroyCollection($id: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + destroyCollection: { id: $id } + } ) { - id - method + uuid + action state } } `, - variables: { - collection_id: 36105 //Specify the collection ID - } + variables: { id: 68844 } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -586,28 +589,30 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation DestroyCollection($collection_id: BigInt!){ - DestroyCollection( - collectionId: $collection_id +mutation DestroyCollection($id: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + destroyCollection: { id: $id } + } ) { - id - method + uuid + action state } } ''' -variables = { - 'collection_id': 36105 #Specify the collection ID -} +variables = {'id': 68844} response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` -Once the transaction is executed, the collection will be destroyed. +Once the transaction is executed, the collection will be destroyed and the will be retrieved. From 2e78eda332afccc4d8c3f2e7ae8f3c61b0c38dae Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Sun, 24 May 2026 15:35:29 +0300 Subject: [PATCH 26/67] Migrate transfer / accept collection ownership page to v3 API Rewrite Step 1 to use the `mutateCollection` discriminator on `CreateTransaction` (setting `owner` to the new owner address) and Step 2 to use the new `acceptCollectionTransfer` discriminator (input shape: `{ id: BigInt! }`). Add SDK warning callout and a brief tip showing the matching `cancelCollectionTransfer` action so an owner can undo a pending transfer. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../010-transfer-accept-collection.md | 217 ++++++++++-------- 1 file changed, 125 insertions(+), 92 deletions(-) diff --git a/docs/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md b/docs/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md index dc2824b..5e0cec5 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md +++ b/docs/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md @@ -15,24 +15,32 @@ import TabItem from '@theme/TabItem'; Collection ownership on the Enjin Blockchain grants specific permissions and control over a collection and its tokens. The collection owner is the only account authorized to make changes to the collection or any tokens within it. This includes the exclusive ability to mint new tokens or mint additional supply of existing tokens in the collection. -Transferring collection ownership is done in two steps: +Transferring collection ownership is a two-step process: the current owner submits a transfer request, and the new owner accepts it. + +:::warning SDKs are not yet available +The C# and C++ SDK examples below are out of date and **will not work against the current Enjin Platform API**. This section will be updated once new SDKs are published. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +::: ## Step #1: Sending a transfer ownership request with the [Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) -To send a transfer ownership request, we use the `MutateCollection` mutation: +To send a transfer ownership request, use the `mutateCollection` discriminator action on `CreateTransaction` and set the `owner` field to the new owner's address: ```graphql mutation SendTransferOwnershipRequest { - MutateCollection( - collectionId: 36105 #Specify the collection ID - mutation: { - owner: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the new owner + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + mutateCollection: { + collectionId: 36105 + owner: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" + } } ) { - id - method + uuid + action state } } @@ -42,8 +50,8 @@ mutation SendTransferOwnershipRequest { ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation SendTransferOwnershipRequest(\r\n $collection_id: BigInt!\r\n $new_owner: String\r\n) {\r\n MutateCollection(\r\n collectionId: $collection_id\r\n mutation: { owner: $new_owner }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"new_owner":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation SendTransferOwnershipRequest($collectionId: BigInt!, $newOwner: String!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: { mutateCollection: { collectionId: $collectionId, owner: $newOwner } }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"collectionId":36105,"newOwner":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"}}' ``` @@ -90,28 +98,29 @@ Work in progress ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` - mutation SendTransferOwnershipRequest( - $collection_id: BigInt! - $new_owner: String - ) { - MutateCollection( - collectionId: $collection_id - mutation: { - owner: $new_owner + mutation SendTransferOwnershipRequest($collectionId: BigInt!, $newOwner: String!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + mutateCollection: { + collectionId: $collectionId + owner: $newOwner + } } ) { - id - method + uuid + action state } } `, variables: { - collection_id: 36105, //Specify the collection ID - new_owner: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the new owner + collectionId: 36105, + newOwner: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" } }), }) @@ -125,28 +134,29 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation SendTransferOwnershipRequest( - $collection_id: BigInt! - $new_owner: String - ) { - MutateCollection( - collectionId: $collection_id - mutation: { - owner: $new_owner + mutation SendTransferOwnershipRequest($collectionId: BigInt!, $newOwner: String!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + mutateCollection: { + collectionId: $collectionId + owner: $newOwner + } } ) { - id - method + uuid + action state } } `, variables: { - collection_id: 36105, //Specify the collection ID - new_owner: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the new owner + collectionId: 36105, + newOwner: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -157,53 +167,77 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation SendTransferOwnershipRequest( - $collection_id: BigInt! - $new_owner: String -) { - MutateCollection( - collectionId: $collection_id - mutation: { - owner: $new_owner +mutation SendTransferOwnershipRequest($collectionId: BigInt!, $newOwner: String!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + mutateCollection: { + collectionId: $collectionId + owner: $newOwner + } } ) { - id - method + uuid + action state } } ''' variables = { - 'collection_id': 36105, #Specify the collection ID - 'new_owner': 'cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f' #Specify the new owner - + 'collectionId': 36105, + 'newOwner': 'cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f' } response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` -Once the transaction is confirmed, the new owner needs to approve the ownership transfer request, for the collection ownership to be executed +Once the transaction is confirmed, the new owner needs to approve the ownership transfer request for the collection ownership to actually move. + +:::tip Cancelling a pending transfer +If you need to undo a pending transfer before the new owner has accepted it, submit a `cancelCollectionTransfer` action with the same collection `id`: + +```graphql +mutation CancelTransferOwnershipRequest { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + cancelCollectionTransfer: { id: 36105 } + } + ) { + uuid + action + state + } +} +``` +::: ## Step #2: Accepting a transfer ownership request with the [Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) -To accept a transfer ownership request, we call the `AcceptCollectionTransfer` mutation from the new collection owner account: +To accept a transfer ownership request, the new owner calls the `acceptCollectionTransfer` discriminator action with the collection's `id`: ```graphql mutation AcceptTransferOwnershipRequest { - AcceptCollectionTransfer( - collectionId: 36105 #Specify the collection ID + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + acceptCollectionTransfer: { id: 36105 } + } ) { - id - method + uuid + action state } } @@ -213,8 +247,8 @@ mutation AcceptTransferOwnershipRequest { ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation AcceptTransferOwnershipRequest($collection_id: BigInt!) {\r\n AcceptCollectionTransfer(collectionId: $collection_id) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation AcceptTransferOwnershipRequest($id: BigInt!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: { acceptCollectionTransfer: { id: $id } }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"id":36105}}' ``` @@ -256,24 +290,24 @@ Work in progress ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` - mutation AcceptTransferOwnershipRequest( - $collection_id: BigInt! - ) { - AcceptCollectionTransfer( - collectionId: $collection_id + mutation AcceptTransferOwnershipRequest($id: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + acceptCollectionTransfer: { id: $id } + } ) { - id - method + uuid + action state } } `, - variables: { - collection_id: 36105 //Specify the collection ID - } + variables: { id: 36105 } }), }) .then(response => response.json()) @@ -286,23 +320,23 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation AcceptTransferOwnershipRequest( - $collection_id: BigInt! - ) { - AcceptCollectionTransfer( - collectionId: $collection_id + mutation AcceptTransferOwnershipRequest($id: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + acceptCollectionTransfer: { id: $id } + } ) { - id - method + uuid + action state } } `, - variables: { - collection_id: 36105 //Specify the collection ID - } + variables: { id: 36105 } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -313,27 +347,26 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation AcceptTransferOwnershipRequest( - $collection_id: BigInt! -) { - AcceptCollectionTransfer( - collectionId: $collection_id +mutation AcceptTransferOwnershipRequest($id: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + acceptCollectionTransfer: { id: $id } + } ) { - id - method + uuid + action state } } ''' -variables = { - 'collection_id': 36105 #Specify the collection ID - -} +variables = {'id': 36105} response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` From 0d6763d32baedd1d451a49586ae52cc795c512b5 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Sun, 24 May 2026 15:49:48 +0300 Subject: [PATCH 27/67] Correct destroy-collection button labels in dashboard flow Update the button names in the Destroying a Collection dashboard steps from "Destroy" to "Destroy Collection" to match the actual UI. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../01-managing-tokens/08-burning-destroying-tokens.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md index b35b75c..eb87a69 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md @@ -67,9 +67,9 @@ To destroy a token, follow the above instructions for Melting a token, but make - If the collection has some tokens, you can follow the above guide [Destroying a token and removing it from the Blockchain](#destroying-a-token-and-removing-it-from-the-blockchain) for each of the tokens in the collection, to destroy them all. ::: -In the Platform menu, navigate to "**[Collections](https://platform.beta.enjin.io/collections)**", **locate the collection** you wish to destroy, click the **3 vertical dots** (**⋮**) on its row, then click "**Destroy**". +In the Platform menu, navigate to "**[Collections](https://platform.beta.enjin.io/collections)**", **locate the collection** you wish to destroy, click the **3 vertical dots** (**⋮**) on its row, then click "**Destroy Collection**". -Confirm by clicking the "**Destroy**" button. +Confirm by clicking the "**Destroy Collection**" button. A **Transaction Submitted** modal appears with the new transaction's UUID and a **View Transaction** button that opens its row on the [Transactions](https://platform.beta.enjin.io/transactions) page. From b1191fe62ff3be5b471e6efabc12ea5d32272dd6 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 25 May 2026 18:32:58 +0300 Subject: [PATCH 28/67] Ignore v3-*.json migration introspection scratch files Working JSONs from schema introspection (v3-introspect-*.json, v3-test-*.json) live next to v3-MIGRATION.md / v3-API.md and shouldn't be tracked any more than the .md scratchpads are. --- .gitignore | 1 + .../01-queries/07-beam-queries.md | 612 ------------------ .../02-mutations/07-beam-mutations.md | 129 ---- 3 files changed, 1 insertion(+), 741 deletions(-) delete mode 100644 docs/03-api-reference/01-queries/07-beam-queries.md delete mode 100644 docs/03-api-reference/02-mutations/07-beam-mutations.md diff --git a/.gitignore b/.gitignore index 4d46dea..4121f1f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ yarn-error.log* # v3 documentation migration working files (not for upstream) v3-*.md +v3-*.json build-test/ diff --git a/docs/03-api-reference/01-queries/07-beam-queries.md b/docs/03-api-reference/01-queries/07-beam-queries.md deleted file mode 100644 index 351e22b..0000000 --- a/docs/03-api-reference/01-queries/07-beam-queries.md +++ /dev/null @@ -1,612 +0,0 @@ ---- -title: "Beam" -slug: "beam" -description: "Query the Enjin API for Beam data, including the status and details of QR code activations for blockchain assets distribution." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - -:::tip GraphQL Endpoint -`https://platform.beta.enjin.io/graphql` -::: - -This is a detailed reference guide that explains the most commonly used operations. - -## GetBeam - -The `GetBeam` query retrieves detailed information about a specific . A Beam is a feature provided by Enjin that allows users to claim blockchain assets via QR codes. This query is essential for applications that need to access and display information about a particular Beam or track its usage. - - - -```graphql -query GetBeam { - GetBeam(code: "0dd3694f2c4599179f08686539cd73dc") { - id - code - name - description - image - start - end - isClaimable - claimsRemaining - qr { - url - payload - } - message { - walletPublicKey - message - } - collection { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - } - } -} - -``` - - -```json -{ - "data": { - "GetBeam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc", - "name": "236", - "description": "This is the most awesome Beam ever.", - "image": "https://en.wikipedia.org/wiki/Firefox#/media/File:Firefox_logo,_2019.svg", - "start": "2022-11-14T00:00:00+00:00", - "end": "2028-11-14T00:00:00+00:00", - "isClaimable": true, - "claimsRemaining": 10, - "qr": { - "url": "https://platform.beta.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc", - "payload": "https://platform.beta.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc" - }, - "message": null, - "collection": null - } - } -} -``` - - - -## GetBeams - -The `GetBeams` query is used to retrieve an array of beam details, optionally filtered by codes or names. This query is particularly useful for applications that need to fetch information about multiple Beams at once, making it easier to manage and display Beam data. - - - -```graphql -query GetBeams { - GetBeams(codes: ["0dd3694f2c4599179f08686539cd73dc"]) { - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - edges { - node { - id - code - name - description - image - start - end - isClaimable - qr { - url - payload - } - collection { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - } - claims { - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - edges { - node { - id - claimedAt - claimStatus - quantity - code - wallet { - account { - publicKey - address - } - } - collection { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - } - } - } - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetBeams": { - "totalCount": 1, - "pageInfo": { - "hasPreviousPage": false, - "hasNextPage": false, - "startCursor": "", - "endCursor": "" - }, - "edges": [ - { - "node": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc", - "name": "236", - "description": "This is the most awesome Beam ever.", - "image": "https://en.wikipedia.org/wiki/Firefox#/media/File:Firefox_logo,_2019.svg", - "start": "2022-11-14T00:00:00+00:00", - "end": "2028-11-14T00:00:00+00:00", - "isClaimable": true, - "qr": { - "url": "https://platform.beta.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc", - "payload": "https://platform.beta.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc" - }, - "collection": null, - "claims": { - "totalCount": 10, - "pageInfo": { - "hasPreviousPage": false, - "hasNextPage": false, - "startCursor": "", - "endCursor": "" - }, - "edges": [ - { - "node": { - "id": 1, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6IllGcEg0L0lRL0x5c1BQeWIyVXdCL0E9PSIsInZhbHVlIjoiVmhtcTRqcUhoeFBPTGQ2QTNWOTQ5aWhOR3poVlJrOFo0QTVHeUpVSUZWcTFwaGR1YW1KMmN5YWJLZ2xKbW1WSSIsIm1hYyI6ImRiMGIyMTAyNjEwOTNkNWUwNTUxZWVkMDhlZGEyZjQ2YmQwMGY5MzhhYjA5ZjBmZmE4MDk4NmNiYTkzM2FiNWQiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 2, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6ImllVllKclNObjk4QXdtZnRjVitLWmc9PSIsInZhbHVlIjoiQlozZzVQSHZWOTZibWxGSkkzcWxBMVNiMkFaZGcxcERGNitBOE1sMFVwZFJsdkE5OXBMVlNlRVZxOGN4eGZaRyIsIm1hYyI6IjI1NzQ5ZjRkOGMzMzBjMWRkNjY1YWZhYjllYjliYmE0OThmYmQ1MGFiMjhhYjhiNTA0ZDlkYWM2OTU5YzJmN2YiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 3, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6IjZYdUVWeTYwK0czRjM2THNHNVBJWFE9PSIsInZhbHVlIjoialhHbS93YkhnNldJa2JHeDFBMCt2V3ZLRDZkVi9INXgvdG1sOGxhSTJwWk12RE1HMEN2ZFZyY2d3dzRDQ0loNCIsIm1hYyI6IjVjOGE0ZGEyYzUzYWQzODgwZDJkNmNlZjM4YjY0YmZmOTcwNTVhNGIyNzUzYWNlYjMxOWNiNGE5OWZhY2NiNmYiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 4, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6Ik9TeDE3ZGMxODFHdXRwUTM4QXp3OUE9PSIsInZhbHVlIjoiaC9mUGtjTkd5RjNFdEk4WGFnNW5QOWJhVFVBR3ZrOXE3bTMrTm9GT01SVVV5NVpVRSt2RGtnQndnTld3TFRlZyIsIm1hYyI6IjdhNjgzYTJhMWQ2NzZlZWM1NTIyZWVkNjFkNzNjNzNlN2JhNmMyZTAyYzhmYTE2M2M1NzNlMTAxNTNlMDI3OGQiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 5, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6IlhYVVF0c0l0ZDNzS2g3a0hLU3JaeVE9PSIsInZhbHVlIjoiWWltZjlvMjJ1M0psRHJPUG1QRmUvVlZnR2RiREQyTWxYVU9pSUtNb2N2MWxqbzFmTVYxM0xiV3cxZWJ4NFg4TyIsIm1hYyI6IjdlOTExYTg5OTIwMDMxMTZkYmJlNGYwZjU5MjhkOWJlMzgyZWVhYzcyMDEwYzRkNzk5Zjk2OWU5YjZjODExNmYiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 6, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6IlVlUjBKeUhrZ092clBqMDVNaTNwOXc9PSIsInZhbHVlIjoiOUNhcUxaQkVDVlZFdnlBM3ZtTUJmWnl0bjN6YVNuUmRXbmoxWXZiOFpZOUFqaW9Od2Z0ZGdWMjF0S2llRHZEUyIsIm1hYyI6Ijg0YjY1ZWEzZGFjOWE5NjI2OTIwMjExNzc2NTNhZWZiMDI3OGQwZGQyZDEzYWJhMjg2OGExZWQyMzJiMmJlM2EiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 7, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6InAxU1I4QzlXcGp2RXRuSENCTkVzcnc9PSIsInZhbHVlIjoiL2J2a0UyK0trd0pJMVJ0QVI0NU9TanFuZmVCcUY3dG5TNW50YURzbXdwUkhBSVIyQTlnQnZHK0trTjh1am9pbiIsIm1hYyI6ImMzYzVkZDZmOTQ2OWRiODBmOTMwMWJkYzZhZTE0ZTkwZTczOGFlZjJkMmYzOTQzZjA1ZGMxNjExMmU5MjRiYjQiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 8, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6InVaRWlMYk1HSjdkZFRhWVlGQlRMK3c9PSIsInZhbHVlIjoidUJjTWpPODZRMThNeUF1T0hHaWJlUTB0a0thTXl5VnZFSHN4UjYzQk5uZ1NQR0lrNWR1NlY1VG5sU2cyZ0R6diIsIm1hYyI6ImZiZjhkY2JhOTY3MmUwZGI2YzYyZmQ4MGFkYmVjMzI2NzRiYWZmOWI4ZjgzMjNkYTRlZTc5MmEyNzZmN2E0MzciLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 9, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6Ik14U2ZndlJiLzdsQktMckdQTGQxeHc9PSIsInZhbHVlIjoiUDBEWGNYcDk1ZExlZlMzNzRPWEdFeFliYmIwWWIxUElzaEpHSkQxb05xN2EyVVVucnd2dWQzTC9Ic3JzYnlSeSIsIm1hYyI6ImY4ODI0OGU0Mzc3NjVkMzZlODljYTI2YzhkNDU3N2MyMmNiZTM0YjZkMWQ3YjU2NjE0NmZlZjkyYjk0NjllOGQiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 10, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6InhiYWZqWlM3YTRhc01EYXZaR2pwRFE9PSIsInZhbHVlIjoiZXlwVytJbWNRT29VOThrR28wWldBa1l3Y3ZPMW5oUmdwUE02Snh1M2FDek4wOXpsS1FEVEFjNU9GV2psNWZCMyIsIm1hYyI6ImI3NTU3YTJjNWQzODQyNjllOGJmZTA5MjBlMjBhYTI3ZTA1Yjg1MmYzN2RiOWQ2MDJjMGRlMmY3Y2YxMDk2ODkiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - } - ] - } - } - } - ] - } - } -} -``` - - - -## GetClaims - -The `GetClaims` query is used to retrieve an array of claim details from a system, and it allows for optional filtering based on claim IDs, codes, accounts, or states. This query is typically used to track and manage claims associated with specific assets or codes in a system. - - - -```graphql -query GetClaims { - GetClaims( - codes: ["0dd3694f2c4599179f08686539cd73dc"] - ) { - edges { - cursor - node { - id - claimedAt - claimStatus - quantity - wallet { - account { - publicKey - address - } - } - collection { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - } - beam { - id - code - } - } - } - totalCount - pageInfo { - startCursor - endCursor - hasPreviousPage - hasNextPage - } - } -} -``` - - -```json -{ - "data": { - "GetClaims": { - "edges": [ - { - "cursor": "eyJpZCI6MTAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "id": 10, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6OSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 9, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6OCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 8, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6NywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 7, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6NiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 6, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6NSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 5, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6NCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 4, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 3, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 2, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6MSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 1, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - } - ], - "totalCount": 10, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasPreviousPage": false, - "hasNextPage": false - } - } - } -} -``` - - - -## GetSingleUseCodes - -The `GetClaims` query retrieves an array of claim details, optionally filtered by claim IDs, claim codes, accounts, or claim states. It is designed to provide information about claims associated with a specific code. - - - -```graphql -query GetSingleUseCodes { - GetSingleUseCodes(code: "7cd390ab3d56c42b818d0a1aa2221c8f") { - edges { - cursor - node { - code - qr { - url - payload - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } -} -``` - - -```json -{ - "data": { - "GetSingleUseCodes": { - "edges": [ - { - "cursor": "eyJpZCI6ODAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "code": "eyJpdiI6IlNFM2tRZzgxaU5pZi9hVXBVZTBCeVE9PSIsInZhbHVlIjoicDdsSmlCU1JHNnd6QUtHU1FRV2pNQ2twaDNPT2YreFVrK3Jrb1VXL0NPc2tOeFBVdTZFM2J1Ykp5NTlRNlNwZXg0d1JTbHo1N1BHMDJqaG11WDhDTGIxRVlVenBoTWVwT3ZrL3RwR2pZTzA9IiwibWFjIjoiYWI3ZjdhY2RmMzE2NGM1MjAxMWVmYTk2ZWNjNWQyNTY4MWQ4NGZhY2ZhN2M5Y2FmMmM0MzU5MDM1N2Q1NDFiYSIsInRhZyI6IiJ9", - "qr": { - "url": "https://platform.beta.enjin.io/claim/eyJpdiI6ImVMNTEybWEwSnZ6WlF3QjUvQW15Wmc9PSIsInZhbHVlIjoiVklOcCtCYVpGTG1aRjQyY0FZb3RmS1JSd1JGT0dka05rT1NQUnZoMXZ4UGsxVklzVmNoejhEUDQ5TjVMc0picjR4MmEzQUJTaWMrUVYxb21yMExZRWVjUGp1T2Rnb0owMHcwWmxXWCtGMnM9IiwibWFjIjoiMjUyNzdlODkyMTRkZmViM2M1OWY5MWNlYjgxYjUzOGE3NTA4NmU0NjY3MjIwOGY0ZDkxNTg3ZTJjOWM5NjUxMSIsInRhZyI6IiJ9", - "payload": "https://platform.beta.enjin.io/claim/eyJpdiI6IkJJWVNYb3lFVGRpaUNEemNJQ20xamc9PSIsInZhbHVlIjoic1ZXMzNGN1FnbWVRc1Z2S2IrYThIT0Rpd2JpeGhza2wxSG1FSlVqTUhzN3UrYmozaXNnZ2dSTU5HTmluZ2laY3JWNUcyemcyaDJZT1VGdGx6SjgxdGVwdUIvN1JhRHRVSnd2RFRYZVhOOUk9IiwibWFjIjoiNzllYmQ1MjFlODBmMWEzOTdjOTc0YWM0YjQ4M2ExZmEyOWZhNzQyY2RlNGJjMzcxNDdkOTQxOWJlYmYyMmUwMSIsInRhZyI6IiJ9" - } - } - }, - { - "cursor": "eyJpZCI6NzEsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "code": "eyJpdiI6Imp5R0xzNC9aVWZlT090SGVsL3RyVlE9PSIsInZhbHVlIjoiemJiWmsrZEl2TXVzc2R1c2svalhIRjdGVUFXRThPOTg2RnZJVCtMNEE1UENxN3FLeURhL1J4UXhBakduekduRmxRQTJwdmRCeXZqQi9rbVVyVnRmK2V4c2xDZU9jUW15N0xNSkRiUyt2ZlE9IiwibWFjIjoiNTZlOGQ4NjdmNDNmOTQ3NWYwM2M2MzRlMGRmODlmNmQwZmM2ZDllNjdmMTk4ZTA0ZWJiYTliZTZkZTJhOTM3MyIsInRhZyI6IiJ9", - "qr": { - "url": "https://platform.beta.enjin.io/claim/eyJpdiI6IlhtTHdEdlM5bXlQNnVEaGZpejkrR3c9PSIsInZhbHVlIjoidlh3dVhuTVhOVEQvcElBM0drMVhBQ2Z1SFdob3BtNmJnYVQwWExoVmlUYWdPTUhkUzJ1QS9JL25yeHVPSnBQb2VyM3lJWXd3aEMySVd1MjkwellTdW50UVl1YTJRdmNCV3lzdXVLaE9Wckk9IiwibWFjIjoiOGNhNmY1MzZlZWRjMmE0ZjMwYjQ0ODY0Nzg5MTk5MDU4OTAwYmZhYjYzODI4OGQ3ZDIyNTQ5MjA2Zjc2MTczYiIsInRhZyI6IiJ9", - "payload": "https://platform.beta.enjin.io/claim/eyJpdiI6IkN4OHZzUDdGc1AwUG9wdXpQSDVGYXc9PSIsInZhbHVlIjoiOTJJRHQ3VjdoZHR0WVpqajVoSGZIZFNoZHJtVE1iRjR0ZndLQXRQYVE1TS8zaGd1WEZDdDQrckNzUXExdnFvZDVVcE5PMVU1bklFRWhqSnVXVi96WVB4RE5CMk1qTFhHYVIvVWdscFljUTA9IiwibWFjIjoiYWIyY2U4YmIzMjJmNjY0ZmE2NzZjMTZhYTE0YjJlMTEwNDQxMjJlY2Y3NzZlYzZkMDNhMGI3NGQ2MDEyYTZlZSIsInRhZyI6IiJ9" - } - } - } - ], - "totalCount": 10, - "pageInfo": { - "hasPreviousPage": false, - "hasNextPage": false, - "startCursor": "", - "endCursor": "" - } - } - } -} -``` - - \ No newline at end of file diff --git a/docs/03-api-reference/02-mutations/07-beam-mutations.md b/docs/03-api-reference/02-mutations/07-beam-mutations.md deleted file mode 100644 index d446966..0000000 --- a/docs/03-api-reference/02-mutations/07-beam-mutations.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -title: "Beam" -slug: "beam" -description: "Perform Beam mutations in the Enjin API to manage QR code activations, enabling blockchain asset distribution via QR code scans." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - -:::tip GraphQL Endpoint -`https://platform.beta.enjin.io/graphql` -::: - -This is a detailed reference guide that explains the most commonly used operations. - -## CreateBeam - -The `CreateBeam` mutation is used to create a new Enjin Beam distribution campaign. Enjin Beam is a method for distributing tokens via QR codes. This mutation sets up the details and parameters of the Beam campaign. - - - -```graphql -mutation CreateBeam { - CreateBeam( - name: "My Beam" - description: "This is a test beam." - image: "https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg" - start: "20230101" - end: "20240101" - collectionId: 7153 - tokens: [ - {tokenIds:"1..10" tokenQuantityPerClaim:1 claimQuantity:1 type:MINT_ON_DEMAND} - ] - ) -} -``` - - -```json -{ - "data": { - "CreateBeam": "cd6443f822cf6e89c03edd4f6fbc6e55" - } -} -``` - - - -## ClaimBeam - -The `ClaimBeam` is used to claim a specific Enjin Beam using a unique code. Enjin Beam is a method for distributing tokens, and this mutation allows users to claim their allocated tokens by providing the necessary code and cryptographic signature. - - - -```graphql -mutation ClaimBeam { - ClaimBeam( - code: "cd6443f822cf6e89c03edd4f6fbc6e55" - account: "cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts" - signature:"0x1d670c35a76efa984fa2bd655d21eb28af0ee470e359848d2105845e170bb589fd6b7c2cbe97c9acce1eb205d4ade39cae75c619230cf44b64b25ec8677d7f0f" - cryptoSignatureType:SR25519 - ) -} -``` - - -```json -{ - "data": { - "ClaimBeam": true - } -} -``` - - - -## UpdateBeam - -The `UpdateBeam` mutation is used to modify the properties of an existing Enjin Beam. It allows you to update specific attributes of a Beam, such as its name, to ensure that the campaign remains accurate and adaptable. - - - -```graphql -mutation UpdateBeam { - UpdateBeam( - code: "ca8524349abab1cc20994841a8e0f175" - name: "My beam UPDATED" - ) -} -``` - - -```json -{ - "data": { - "UpdateBeam": true - } -} -``` - - - -## DeleteBeam - -The `DeleteBeam` mutation is used to delete an existing Enjin Beam from the system. It allows you to remove a Beam and all of its associated data based on its unique identifier code. - - - -```graphql -mutation DeleteBeam { - DeleteBeam(code: "cd6443f822cf6e89c03edd4f6fbc6e55") -} -``` - - -```json -{ - "data": { - "DeleteBeam": true - } -} -``` - - From 3efc2e8247fa5e49b2df36e4519bdc71e2cbe5c2 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 25 May 2026 18:33:46 +0300 Subject: [PATCH 29/67] Flag fuel-tank API reference as out-of-date pending rebuild Per Enjin team direction the fuel-tank API surface is being rebuilt, and the v2 queries/mutations on these pages won't work against the current platform. Add a warning callout at the top of both reference pages so readers know the examples are stale; revisit once the new API lands. --- docs/03-api-reference/01-queries/05-fuel-tank-queries.md | 4 ++++ docs/03-api-reference/02-mutations/05-fuel-tanks-mutations.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/03-api-reference/01-queries/05-fuel-tank-queries.md b/docs/03-api-reference/01-queries/05-fuel-tank-queries.md index f45b694..4865355 100644 --- a/docs/03-api-reference/01-queries/05-fuel-tank-queries.md +++ b/docs/03-api-reference/01-queries/05-fuel-tank-queries.md @@ -16,6 +16,10 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere `https://platform.beta.enjin.io/graphql` ::: +:::warning Fuel Tank API is being updated +The Fuel Tank API is currently being rebuilt — the queries on this page are out of date and **will not work against the current Enjin Platform API**. Refer back to this page once support lands. +::: + This is a detailed reference guide that explains the most commonly used operations. ## GetAccounts diff --git a/docs/03-api-reference/02-mutations/05-fuel-tanks-mutations.md b/docs/03-api-reference/02-mutations/05-fuel-tanks-mutations.md index 9e88c3a..7ebbc05 100644 --- a/docs/03-api-reference/02-mutations/05-fuel-tanks-mutations.md +++ b/docs/03-api-reference/02-mutations/05-fuel-tanks-mutations.md @@ -16,6 +16,10 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere `https://platform.beta.enjin.io/graphql` ::: +:::warning Fuel Tank API is being updated +The Fuel Tank API is currently being rebuilt — the mutations on this page are out of date and **will not work against the current Enjin Platform API**. Refer back to this page once support lands. +::: + This is a detailed reference guide that explains the most commonly used operations. ## CreateFuelTank From ac0de31cfe8ebdc6a91af670d5f831b6ef1517a9 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 25 May 2026 18:36:11 +0300 Subject: [PATCH 30/67] Migrate API reference overview to v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the v2 GraphiQL walkthrough — that material is already covered in depth by the How to Use GraphQL primer, so duplicating it here was mostly noise. Reshape the page as a true landing/index for the API Reference section: list the resource pages, surface the network/chain arguments, and show the canonical CreateTransaction discriminator pattern with a correct v3 example (uuid/action, flat tokenId, no mintPolicy wrapper). --- docs/03-api-reference/03-api-reference.md | 97 +++++++++-------------- 1 file changed, 37 insertions(+), 60 deletions(-) diff --git a/docs/03-api-reference/03-api-reference.md b/docs/03-api-reference/03-api-reference.md index 2256a6b..f2a7244 100644 --- a/docs/03-api-reference/03-api-reference.md +++ b/docs/03-api-reference/03-api-reference.md @@ -2,89 +2,66 @@ title: "API Reference" slug: "/api-reference" --- -The Enjin API provides a robust GraphQL interface for interacting with the Enjin Platform. This guide will demonstrate how to explore available queries and mutations using the GraphiQL Playground. -## Exploring the GraphiQL Playground +This section is a per-resource reference for the Enjin Platform GraphQL API. Each page focuses on one resource family (accounts, collections, tokens, transactions, marketplace listings, fuel tanks, token groups, nomination pools) and walks through the queries and mutations available for it. -The GraphiQL Playground is an interactive environment where developers can explore the full range of API functionalities and build queries/mutations. Here’s how you can navigate and utilize this tool effectively. - -### Accessing the GraphiQL Playground - -You can access the GraphiQL Playground here: [Core Operations](https://platform.beta.enjin.io/graphiql). - -### Navigating the Documentation Explorer - -#### Step 1: Open the Documentation Explorer - -- Start by opening the GraphiQL Playground and click the `Show Documentation Explorer` button located at the top left. This will display a panel listing all available queries, mutations, and types. - -![Show Documentation Explorer](/img/getting-started/show-documentation-explorer.gif) - -#### Step 2: Search for a Specific Mutation - -- In the Documentation Explorer, you can use the search input to find a specific mutation. For example, to find the "CreateToken" mutation, type "Create" into the search bar. This helps narrow down the list to relevant operations. - -![Search mutations or queries](/img/getting-started/search.gif) - -:::tip Tip: quick Access to Documentation -If you already have a query or mutation in your builder, you can quickly open its documentation page by clicking on it while holding the `Ctrl` button. This will navigate you directly to its details in the Documentation Explorer. +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` ::: -#### Step 3: Explore the Mutation Details +## How this section is organised -- Click on the "CreateToken" mutation to view detailed information about it. You will see: - - **Overview**: A description of what the mutation does. - - **Return Type**: The type of data it returns, such as a "transaction". - - **Arguments**: A list of arguments it accepts, with descriptions and data types. +- **Queries** — read on-chain and platform state: [transactions](/03-api-reference/01-queries/01-transactions-queries.md), [collections](/03-api-reference/01-queries/02-collections-queries.md), [tokens](/03-api-reference/01-queries/03-tokens-queries.md), [accounts](/03-api-reference/01-queries/04-wallets-queries.md), [fuel tanks](/03-api-reference/01-queries/05-fuel-tank-queries.md), [marketplace](/03-api-reference/01-queries/06-marketplace-queries.md). +- **Mutations** — submit on-chain actions and manage platform-side resources: [transactions](/03-api-reference/02-mutations/01-transaction-mutations.md), [collections](/03-api-reference/02-mutations/02-collections-mutations.md), [tokens](/03-api-reference/02-mutations/03-tokens-mutations.md), [wallets](/03-api-reference/02-mutations/04-wallets-mutations.md), [fuel tanks](/03-api-reference/02-mutations/05-fuel-tanks-mutations.md), [marketplace](/03-api-reference/02-mutations/06-marketplace-mutations.md). +- [**Important Arguments**](/03-api-reference/04-important-arguments.md) — a glossary of recurring argument types and enum values (`network`, `chain`, `state`, `cap`, etc.) used throughout the API. -![Explore mutation, query, or type](/img/getting-started/explore-mutation-query-type.gif) +If you're new to GraphQL or to the Enjin API, start with [Using the Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) and the [How to Use GraphQL](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md) primer — they cover authentication, the playground, and operation structure. -### Understanding Arguments and Types +## Two top-level shapes worth knowing -#### Example: Creating a Token with Initial Supply of 10 +Two things are common to almost every example in this reference and worth understanding before you read them. -- For instance, if you want to create a token with an initial supply of 10, you will need to focus on the `params` argument. This argument is of type "CreateTokenParams", a complex type. +### 1. `network` and `chain` arguments -

- Example of exploring an argument -

+Every operation that touches the blockchain takes a `network` and a `chain` argument: -- Click on `CreateTokenParams` to explore its fields: - - **tokenId**: Of type "EncodableTokenIdInput", which specifies the token identifier. - - **initialSupply**: Of type "BigInt", which you would set to 10. - - **cap**: Of type "TokenMintCap", which may define the maximum supply (optional). +- `network: ENJIN` (mainnet) or `network: CANARY` (testnet). +- `chain: MATRIX` (Matrixchain — collections, tokens, marketplace) or `chain: RELAY` (Relaychain — staking, governance). -

- Example of exploring a complex type argument -

+A single endpoint serves both networks and both chains; you select which to target on each request. -#### Constructing Your Mutation +### 2. `CreateTransaction` — one mutation for every on-chain action -- Using the information from the Documentation Explorer, you can construct a mutation to create a token. Here is an example: +Almost every on-chain action — creating a collection, minting tokens, transferring, listing on the marketplace, bonding to a nomination pool — is submitted through the single `CreateTransaction` mutation. The specific action is selected by which field is set on the `transaction` input. + +For example, creating a token looks like this: ```graphql mutation { - CreateToken( - recipient: "efRecipientAddress" - collectionId: "12345" - params: { - tokenId: { integer: 1 } - initialSupply: 10 + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + createToken: { + recipient: "efRecipientAddress" + collectionId: 12345 + tokenId: 1 + initialSupply: 10 + listingForbidden: false + infusion: 0 + anyoneCanInfuse: false + } } ) { - id - method + uuid + action state } } ``` -- This mutation creates a new token with an initial supply of 10 in the specified collection. Adjust the parameters as needed for your application. +The same mutation handles `mintToken`, `transferToken`, `createCollection`, `createListing`, and ~40 other actions — see [Transaction Mutations](/03-api-reference/02-mutations/01-transaction-mutations.md) for the full surface and [Important Arguments](/03-api-reference/04-important-arguments.md) for the shapes returned. -:::tip Not sure how to construct the query/mutation? -Check out our [GraphQL Query Builder Guide](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#graphiql-playground) for detailed instructions on building queries and mutations. +:::tip Not sure how to construct an operation? +The [GraphQL Query Builder](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#graphiql-playground) section walks through using the GraphiQL Explorer to build queries and mutations interactively. ::: - -#### Conclusion - -By using the Documentation Explorer in the GraphiQL Playground, you can efficiently navigate and understand the Enjin API's queries, mutations, and field types. From 42a8b5875051e639367f417bdd02fed9f82a3350 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 25 May 2026 18:39:21 +0300 Subject: [PATCH 31/67] Rewrite Important Arguments reference for v3 Drop v2-only args (verificationId, after/first cursors, transactionId, signedAtBlock, method, acknowledgeEvents, simulate, skipValidation, continueOnFailure, keepAlive, source, operator, freezeType, the entire Beam section), refresh the remaining ones to v3 names (uuid, action, extrinsicHash, signerAddress, signerExternalId, recipients[].address, flat tokenId BigInt, cap enum without INFINITE/SINGLE_MINT, state enum with TIMEOUT and no PROCESSING), and add the v3-new args (network, chain, limit/page/cursor, infusion, anyoneCanInfuse, removeTokenStorage, groupIds, listingData, usesWhitelist, fill, unbondingPoints, proxyAddress, fuelTank). Preserves every anchor that is referenced by already-migrated guide pages: #state, #cap, #supply, #collapsing_supply, #attributes, #mintpolicy, #marketpolicy, #explicitroyaltycurrencies, #hasroyalty, #tokenid. Strips the now-broken Beam-anchor links in create-qr-drops (that page is being hidden separately). --- .../01-managing-tokens/06-create-qr-drops.md | 8 +- .../04-important-arguments.md | 395 +++++++----------- 2 files changed, 155 insertions(+), 248 deletions(-) diff --git a/docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md b/docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md index 3751ea2..55bf39c 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md +++ b/docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md @@ -43,15 +43,15 @@ Now, you'll fill out the details for your Beam: - **Tokens to Distribute:** This is where you define which tokens are given out and how many. - A single Beam is made up of one or more "Beam Claims". Each time a user scans the QR code and successfully receives a token, they are redeeming one **Beam Claim**. - **[Token IDs](/03-api-reference/04-important-arguments.md#tokenid):** Specify the IDs of the specific tokens you want to distribute with this claim. - - **[Claim Quantity](/03-api-reference/04-important-arguments.md#claimquantity):** This sets how many individual Beam Claims you want to create for each selected Token ID. - - **[Token Quantity Per Claim](/03-api-reference/04-important-arguments.md#tokenquantityperclaim):** This sets how many units of the token a user receives *each time* they redeem a single Beam Claim. For example, if you're giving out stacks of 5 gold coins with each claim, set this to 5. - - **[Type](/03-api-reference/04-important-arguments.md#beam-claim-type):** Choose whether redeeming a claim will `MINT` a brand new token or `TRANSFER` an existing token from your account balance. + - **Claim Quantity:** This sets how many individual Beam Claims you want to create for each selected Token ID. + - **Token Quantity Per Claim:** This sets how many units of the token a user receives *each time* they redeem a single Beam Claim. For example, if you're giving out stacks of 5 gold coins with each claim, set this to 5. + - **Type:** Choose whether redeeming a claim will `MINT` a brand new token or `TRANSFER` an existing token from your account balance.

Create Beam Form 2

-:::tip Use **[Flags](/03-api-reference/04-important-arguments.md#flags)** for advanced options and more control +:::tip Use **Flags** for advanced options and more control - **Single-use:** This important flag changes how the QR code works. - **Default (Single-use OFF):** You get one main QR code for the entire Beam. When a user scans it, the system randomly picks one of the available Beam Claims to give them. - **Single-use ON:** A *unique QR code* is generated for *every single Beam Claim*. diff --git a/docs/03-api-reference/04-important-arguments.md b/docs/03-api-reference/04-important-arguments.md index d95b179..b11551d 100644 --- a/docs/03-api-reference/04-important-arguments.md +++ b/docs/03-api-reference/04-important-arguments.md @@ -1,7 +1,7 @@ --- title: "Arguments" slug: "important-arguments" -description: "A quick reference for some important arguments used in the Enjin API Schema." +description: "A quick reference for important arguments and enum values used throughout the Enjin API." --- import GlossaryTerm from '@site/src/components/GlossaryTerm'; @@ -11,424 +11,331 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere 🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. ::: -This is a detailed reference guide that explains most common arguments within the Enjin API Schema. +A glossary of recurring argument types, fields, and enum values you'll see across the Enjin API. -## id +## network -The `id` argument is a unique identifier used to specifically identify a query or mutation that occurred on the Enjin Platform. +Selects which network the operation targets: -It helps pinpoint a particular operation when you want to reference it or perform actions related to that operation. +- `ENJIN` — Enjin Blockchain mainnet. +- `CANARY` — Enjin Canary testnet. -This identifier is typically generated by the Enjin API and serves as a reference point for interacting with specific operations. +The same endpoint serves both networks; pass `network` on every query and mutation to choose which one. -## transactionId +## chain -A convenient way to rapidly reference blockchain transactions in a sequential order. +Selects which chain within the network the operation targets: -It is structured in the format `xxxxxx-y`, where `xxxxxx` denotes the block number, and `y` indicates the position of the transaction within that block. +- `MATRIX` — Matrixchain (collections, tokens, marketplace, fuel tanks). +- `RELAY` — Relaychain (staking, nomination pools, governance). -## transactionHash +## uuid -The "transactionHash" argument in GraphQL serves as a unique identifier for a particular blockchain transaction. - -It is generated using a cryptographic hash function, guaranteeing that each "transactionHash" is entirely distinct and cannot resemble an identifier from a different blockchain network. +The unique identifier returned for every transaction created via `CreateTransaction` / `CreateBatchTransaction`. Use it to look the transaction up later with `GetTransaction(uuid:)` or to sign it externally with `SignTransaction(uuid:, signedExtrinsic:)`. ## idempotencyKey -A special key that prevents the same operation from being accidentally carried out more than once. It ensures that if you send the same request multiple times, it has the same effect as sending it once, avoiding unintended duplicates. - -## verificationId - -A unique identifier that is returned once you verify a user's ownership over a specific wallet. - -You will need to use the `verificationId` to find a wallet address once it's been verified but you can also use it to find other important information regarding the wallet. - -## after +A client-supplied key that prevents the same operation from being submitted twice. Send the same `idempotencyKey` on a retried request and the server returns the original transaction instead of creating a new one. -Used as a cursor to specify a starting point for fetching data in a paginated list. +## extrinsicHash -It helps you retrieve data after a specific item or position in the list. +The cryptographic hash of the signed extrinsic, populated once a transaction is broadcast to the chain. Use it to look up the transaction on a block explorer like Subscan, or to filter transactions via `GetTransactions(extrinsicHashes: [...])`. -## first - -Used to specify the number of results or items you want to return per page when dealing with paginated data. - -Simply provide it with a numeric value that represents the desired number of results you want to retrieve in a single page. - -## account - -Your "Enjin Blockchain account" that acts like your digital identity on the blockchain. +## externalId -It allows you to interact with the blockchain, such as sending or receiving tokens. +A free-form string you supply when creating a Managed Wallet (`CreateManagedWallet(externalId:)`). Use it to map a wallet to a record in your own database — `GetManagedWallet(externalId:)` returns the wallet's public key. -Think of it as your account on the blockchain network. +## address -### address +A human-readable SS58 representation of a chain account, e.g. `efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y`. Use it as the lookup key for `GetAccount(address:)` and as the `recipient` field on transfer / mint actions. -A human-readable representation of your Enjin Blockchain account. +## publicKey -Users typically interact with and see the address as their primary identifier in wallets and marketplaces. +The hex-encoded public key associated with an account (e.g. `0xded3c8…`). Managed Wallets expose their `publicKey`; you can pass it interchangeably with `address` as a `recipient` value. -It's often in the form of a long alphanumeric string, making it easier for people to send you tokens. +## limit -Your address is derived from your public key and serves as a more user-friendly way to identify your account. +The page size for paginated queries (`GetTokens`, `GetCollections`, `GetListings`, `GetTransactions`, `GetManagedWallets`, `GetNominationPools`). Required on every paginated request. -### publicKey +## page -A cryptographic key associated with your Enjin Blockchain account. +The 1-based page number for offset-paginated queries (`GetTokens`, `GetCollections`, `GetListings`, `GetNominationPools`). Increment until the returned list is empty. -It serves as your unique identifier on the blockchain. +## cursor -When someone wants to send you tokens or verify your transactions, they use your public key to do so. It's like your digital fingerprint. +The opaque pagination token used by cursor-paginated queries (`GetTransactions`, `GetManagedWallets`). Pass back the `nextCursor` from the previous response; a `null` `nextCursor` indicates the last page. ## hashes -Allows you to filter blockchain transactions based on their unique transaction hashes. +A list of extrinsic hashes — used to filter `GetExtrinsics(hashes: [...])` to specific on-chain extrinsics. -Transaction hashes are cryptographic representations of individual transactions on the blockchain, ensuring their immutability and uniqueness. +## ids -## numbers +A list of identifiers — used by: -Used for filtering transactions by block numbers, allows you to specify the block numbers from which you want to retrieve transactions. - -This is useful when you want to narrow down and retrieve transactions from specific blocks on the blockchain. +- `GetBlocks(ids: [...])` to fetch specific blocks by number. +- `GetCollections(ids: [...])` to fetch specific collections by id. +- `GetPendingCollectionTransfers(ids: [...])` to filter to specific pending transfers. ## collectionId -A defining identifier for token collections on the Enjin Blockchain. It is automatically generated by the blockchain when a new collection is created. - -Token collections play a pivotal role in organizing and managing related tokens, simplifying their accessibility and interaction within wallets and on marketplaces. - -## acknowledgeEvents - -Controls whether to automatically acknowledge events that were broadcast to the blockchain but have not yet been acknowledged by the blockchain itself. - -When events are acknowledged before finalization on the blockchain, the operation's `result` is set as `EXTRINSIC_FAILED`, indicating a failure in the blockchain's processing. +The unique identifier of a token collection on the Enjin Blockchain. Generated when a collection is created and used everywhere you reference tokens within that collection (`GetTokens`, `mintToken`, `transferToken`, etc.). ## tokenId -Used to request specific information about a particular token on the Enjin Blockchain. +The unique identifier of an individual token within a collection. In the Enjin Platform API `tokenId` is a flat `BigInt` scalar — pass a plain number (e.g. `tokenId: 10`), not a wrapper object. -It allows you to retrieve details, properties, and attributes of the token identified by its unique token ID. +`GetToken` accepts either `(collectionId:, tokenId:)` or a single canonical `id: "-"`. -### integer - -A straightforward numerical identifier for a token. It's a simple and efficient way to represent tokens. +The token id space supports several encoding strategies that map to different developer ergonomics: -You might choose `integer` when you want a basic, numeric representation of tokens without additional complexity. +### integer -It's useful for scenarios where simplicity and efficiency are more important than encoding additional information in the ID. +A plain numeric token id. Simple, efficient, and the right default unless you need something more structured. ### stringId -Derived from a string, such as a name or label. It you to choose human-readable representations of tokens. - -You might choose `stringId` when you want to represent tokens with user-friendly names or labels. - -It's useful for mapping easily recognizable names. +A token id derived from a human-readable string, useful when you want token ids that map cleanly to names or labels. ### hash -A `hash` is created using a cryptographic hash function, ensuring uniqueness and collision resistance. - -It's suitable when uniqueness across collections is critical. - -It's often used in cases where tokens must be resistant to duplication or tampering. +A token id derived from a cryptographic hash, useful when you need uniqueness across many independent inputs without coordinating ids. ### ERC1155 -Designed to facilitate seamless conversion between tokens Ethereum Virtual Machine (EVM) chains and tokens on Enjin Blockchain. - -Particularly useful for projects that require interoperability between blockchain ecosystems. - -It enables tokens to be converted from one chain to another while retaining their essential properties, making it a valuable tool for cross-chain token management and exchange. +A token id encoded in the ERC-1155 layout — combining a 112-bit base id with a 16-bit index — for projects that need round-trip compatibility with EVM-side token ids. #### index -A 16-bit integer that represents a specific instance or variation of a token type within an ERC-1155 contract. - -It is used in combination with the base token ID to uniquely identify individual tokens of the same type within the contract. +The 16-bit instance within an ERC-1155 token id — combined with the base id to identify a specific token under the same base. #### tokenId -A unique identifier used within the ERC-1155 token standard. - -It combines two components: a 16-bit hexadecimal base token ID and a 16-bit integer index. - -When these components are combined, they create a 128-bit integer that serves as a distinct identifier for a specific token within an ERC-1155 contract. - -## method - -Enables precise and efficient retrieval of transaction-specific method. +In the ERC-1155 layout, the combined 128-bit token id formed from the 112-bit base and 16-bit index. -This targeted approach offers a clear and detailed understanding of the exact actions performed within a transaction, simplifying debugging, security auditing, and user interaction. +## id (composite token id) {#id} -It streamlines data retrieval by focusing on the essential method details, making it a powerful tool for developers, auditors, analysts, and end-users to gain insights, enhance security, and improve the overall user experience in blockchain applications. +For `GetToken`, the optional `id` argument is the canonical token id string `"-"`, e.g. `"7153-10"`. Use it as an alternative to passing `collectionId` + `tokenId` separately. -## signedAtBlock +## action -Specifies the exact block at which a transaction was signed on the blockchain. - -It serves as a historical reference point, indicating precisely when the transaction was initiated and digitally signed within the blockchain's timeline. - -This information is particularly useful when there is a need find a transaction within a block, verify the authenticity of a digital signature, or ensure compliance with timestamp-related requirements. - -It enables users, developers, and auditors to track the temporal context of blockchain interactions, offering a clear historical record that can be crucial for auditing, legal, regulatory, and security purposes. +The runtime extrinsic name for a transaction, e.g. `MultiTokens.create_collection`, `MultiTokens.mint`, `Marketplace.create_listing`. Returned on the `Transaction` type and useful for distinguishing transactions in lists / dashboards. ## state -Used for tracking and monitoring the progress of Enjin Platform queries and mutations, offering insights into their current status and history. Here are the various states and why they are useful: +The lifecycle state of a transaction, returned on the `Transaction` type. Possible values: -- **`ABANDONED`:** Indicates that the operation couldn't be sent, often due to encoding issues or insufficient funds. -- **`PENDING`:** Represents a newly created operation that is awaiting approval by a wallet. -- **`PROCESSING`:** Signifies that the Enjin Wallet Daemon has picked up the transaction and is actively processing it. -- **`BROADCAST`:** Indicates that the transaction has successfully sent the transaction to the Enjin Blockchain. -- **`FINALIZED`:** Denotes that the transaction has been successfully included in a block on the blockchain. Although, it doesn't specify whether the on-chain transaction succeeded or failed. That data can be queried by using the `result` field. +- **`PENDING`** — the transaction has been created on the platform and is awaiting signing/broadcast. +- **`BROADCAST`** — the signed transaction has been submitted to the blockchain. +- **`FINALIZED`** — the transaction has been included in a block. Check the on-chain extrinsic outcome (e.g. on Subscan) to confirm success vs. failure. +- **`FAILED`** — the transaction was rejected by the blockchain. +- **`ABANDONED`** — the platform couldn't sign or send the transaction (typically encoding issues or insufficient funds). +- **`TIMEOUT`** — the transaction was broadcast but not finalized within the timeout window. -## result +## signerAddress -Indicates whether a transaction has succeeded (`ExtrinsicSuccess`) or failed (`ExtrinsicFailed`). +On `CreateTransaction` / `CreateBatchTransaction`, the address of the account that should sign the transaction. Defaults to the Wallet Daemon's address. Set it to a Managed Wallet's public key (or external user address, with `SignTransaction` used to submit the signed extrinsic) to sign with a different account. -This is invaluable for transaction verification and error handling. +## signerExternalId -It offers transparency and user confidence in blockchain interactions, aids in security audits and compliance checks, and enables automated processes to make decisions based on transaction outcomes. +The `externalId` of a Managed Wallet — an alternative to `signerAddress` that doesn't require a separate `GetManagedWallet` lookup. The platform resolves the externalId to the matching managed wallet and signs the transaction with it. -This single piece of information is pivotal for ensuring the reliability, integrity, and functionality of blockchain-based applications and systems, making it an essential component in blockchain transaction processing. +## proxyAddress -## externalId +On `CreateTransaction` / `CreateBatchTransaction`, the address of a Substrate proxy account. When set, the transaction is wrapped in a `proxy.proxy` call so it executes on behalf of the proxied account while being signed by the proxy. -You can utilize the `externalId` argument to establish a connection between an account in database and an Enjin Platform Account. +## fuelTank -It's worth noting that you have control over setting a user's `externalId`, so it's advisable to choose an identifier that aligns with your existing database, making "`externalId`" a valuable and versatile option for processing requests. +On `CreateTransaction` / `CreateBatchTransaction`, the address of an existing fuel tank to dispatch the transaction through. The tank pays the transaction's fees rather than the signer. ## signature -Allows for message authentication, ensuring that data remains unaltered during transmission, and guarantees the integrity of blockchain-stored data. - -Additionally, `signature` serves as a means of verifying user identity and authorization, providing non-repudiation, enhancing security, and confirming the legitimacy of blockchain transactions. - -Overall, this argument is indispensable for maintaining trust, security, and data reliability in cryptographic and blockchain environments. - -## uuids - -You would use the "uuids" argument in an argument to acknowledge and remove specific events from a cache, ensuring cache integrity and efficient system operation by eliminating unnecessary data. - -## expiration - -You use the `expiration` argument when authorizing another account to manage tokens from a specific collection for a limited time, ensuring that the approval automatically expires at the specified block number, providing control and security in scenarios involving NFTs or tokenized assets without a permanent transfer of ownership. - -## operator - -Use the `operator` argument to grant specific accounts the authorization to manage and operate a token collection, allowing for delegated token management without transferring ownership. - -## signingAccount +A cryptographic signature produced by the holder of an account's private key. Used by: -Use the `signingaccount` argument to specify the account responsible for signing and authorizing a particular action or transaction, ensuring security, accountability and traceability in the process. +- `VerifyMessage(publicKey:, signature:, message:)` — proves the signer owns the address that corresponds to `publicKey` by verifying the signature against `message`. +- `SignTransaction(uuid:, signedExtrinsic:)` — submits an externally-signed extrinsic for a transaction created with `signerAddress: `. -## simulate +## recipient -You would use the `simulate` parameter set to `true` when you want to test and validate the outcome of a transaction without actually executing it on the blockchain network. - -It provides a valuable way to safely experiment with transactions and prevent unintended consequences, especially in scenarios where accuracy and precision are critical. +The address (or hex public key) that will receive minted, created, or transferred tokens. Required on `mintToken`, `transferToken`, `createToken`, and per-entry on `mintTokens` / `createTokens`. ## recipients -You can use the **recipients** parameter for transferring or minting tokens, to specify the accounts to which the tokens will be transferred, enabling targeted and controlled token transfers to designated recipients. +The batched form of `recipient`. On `batchTransfer`, supply `recipients: [{ address, tokenId, amount }, ...]` to transfer multiple tokens to multiple destinations in a single transaction. The per-entry field is `address`, not `recipient`. ## attributes -use on-chain attributes to assign and manage metadata, enabling detailed and comprehensive token metadata definition, which is valuable for providing rich and descriptive information about the token on the blockchain. +On-chain key/value metadata you can attach to a collection, token, or token group. Stored on-chain; useful for storing the metadata URI and other small descriptive fields. ### key -A "key" serves as the identifier or label for a specific attribute. +The identifier for an attribute (e.g. `"uri"`, `"name"`). ### value -A "value" represents the corresponding information or data associated with that attribute. +The data stored under that attribute (e.g. a metadata URI, a numeric stat, a flag). ## hasRoyalty -Configures royalties for each marketplace sale of this token. -Royalties are effectively enforced by the core blockchain, ensuring you receive your entitled earnings whenever the token is involved in transactions, providing a transparent and automated system for royalty distribution. +A token can be configured to pay a royalty on every marketplace sale. Royalties are expressed via the token's `behavior` field: + +```graphql +behavior: { + type: HAS_ROYALTY + royalties: [ + { address: "efRoyaltyBeneficiary", percentage: 5.0 } + ] +} +``` + +The chain enforces the royalty automatically when the token is sold. - #### beneficiary - - The recipient or entity designated to receive a portion of the proceeds from token sales + The address that receives the royalty share. Set via `royalties[].address` inside the token's `behavior`. - #### percentage - - Represents the specific portion or share of the transaction value that is allocated to the beneficiary. + The percentage of each sale paid to the beneficiary (as a Float, e.g. `5.0` for 5%). ## isCurrency -This concept is used by the marketplace to determine what assets are allowed to be listed, and which side of the listing is charged fees. +A token can be configured as a currency token that other marketplace listings can be priced in. Currency behavior is set via the token's `behavior` field: + +```graphql +behavior: { + type: IS_CURRENCY + name: "MyGameGold" + symbol: "MGG" + decimalCount: 4 +} +``` ## cap -Employed to limit the maximum supply of tokens, creating scarcity and rarity by setting an upper bound on the number of tokens that can ever be created. +Limits how many tokens of a given id can exist. Set the `cap` field on `createToken` / `createTokens`; leave it unset for an unlimited supply. - #### SUPPLY - - A fixed supply model where the cap amount sets the maximum tokens that can exist in circulation. Burned tokens can be re-minted. + A fixed-supply cap. The collection owner can mint up to the `supply` value; burned tokens reduce the circulating supply and can be re-minted, so the cap holds steady over time. - #### COLLAPSING_SUPPLY - - A dynamic supply model where the cap amount defines the total tokens that can ever be minted. Burned tokens cannot be re-minted. - -- #### SINGLE_MINT (DEPRECATED) - - - Allows the token to be minted only once. Functions as a collapsing supply model with the cap amount set to the initial supply. - -- #### INFINITE - - - Allows unlimited token creation with no supply cap. + A non-increasing cap. Each burn permanently reduces the cap by the burned amount, so re-minting burned tokens is not possible. ### type -defines the token's minting behavior, where `INFINITE` allows unlimited minting, `SINGLE_MINT` enables minting of a single token, and `SUPPLY` specifies a fixed supply of tokens. +Set `cap.type` to either `SUPPLY` or `COLLAPSING_SUPPLY`, and `cap.supply` to the numeric cap value. ## amount -Represents a numerical value associated with the quantity of a token, such as the quantity available, the total supply, or the number of tokens to be minted, depending on the context of its use. +A numeric quantity associated with a token operation — the number of units to mint, transfer, burn, infuse, list, bid, etc. ## freezeState -Allows you to define the duration of the suspension of transferability, with `PERMANENT` indicating a long-term freeze, `TEMPORARY` signifying a short-term suspension, and `NEVER` indicating that the token or collection will never be frozen, providing flexibility in asset management. +Used by `freezeToken` / `thawToken` to specify the lock duration: + +- `NEVER` — the token can never be frozen. +- `PERMANENT` — the token is locked indefinitely. +- `TEMPORARY` — the token is locked for a defined period. ## initialSupply -Specifies the quantity of tokens that will be minted and delivered to the designated recipient. +On `createToken` / `createTokens`, the quantity of the new token minted to the `recipient` at creation time. ## listingForbidden -Setting `listingForbidden` to true indicates that the token is restricted from being listed on a marketplace, providing control over its availability and preventing it from being traded or displayed on external platforms. +On `createToken` / `mutateToken`, set to `true` to block the token from being listed on any marketplace. ## unitPrice -The `unitPrice` of a token is critical as it determines the cost structure, ENJ backing, and accessibility of the token, with the added significance that it can be increased but never decreased, requiring alignment with the token's distribution goals. - -## signingAccount - -The "signingAccount" parameter should be configured with the address of the wallet that owns the token, ensuring that the actions and transactions associated with the token are appropriately authorized and executed by the correct account. - -## skipValidation - -The `skipValidation` option, when set to true, allows for the bypassing of validation checks, which should be exercised with caution and only in scenarios where custom validation or specific requirements necessitate the bypassing of standard validation rules. - -## continueOnFailure +On `mintToken` / `mintTokens`, an optional per-unit price (in the smallest ENJ unit) used to set the token's per-unit deposit. Once set, the unit price can only be increased, never decreased. -The `continueOnFailure` parameter, when set to true, indicates that the system should proceed with the transaction even if it encounters a failure or error during execution, offering a graceful handling of errors and allowing the transaction to continue despite issues. When set to false, any failure during execution will result in the transaction being halted and not proceeding further. - -## keepAlive - -When conducting Enjin Coin (ENJ) transfers, utilizing the "keepAlive" option with a value of true is advisable if you intend to preserve the sender's Enjin Coin Account, even when its balance drops below the minimum threshold of 0.1 ENJ. +## owner -It's important to recognize that Enjin Coin Accounts and Token Accounts are distinct entities, meaning that a wallet can still hold tokens and NFTs even if the Enjin Coin Account is removed. +On `mutateCollection`, set `owner: ` to initiate a collection transfer. The new owner must then call `acceptCollectionTransfer(id:)` to complete it; the current owner can undo the pending transfer with `cancelCollectionTransfer(id:)` before it's accepted. Pending transfers are queryable via `GetPendingCollectionTransfers`. -## source +## expiration -The `source` parameter refers to the address from which the tokens will be transferred, representing the ownership address of the tokens and the origin of the transfer. +On listings, an optional block-number expiry — once that block is reached the listing is no longer fillable. Set inside `listingData.expiration` when calling `createListing`. ## mintPolicy -The "mintPolicy" parameter allows you to define the minting policy for tokens within a collection, with the following sub-parameters: +Returned on `Collection.mintPolicy`. The mint-policy fields are set directly on `createCollection` (no `mintPolicy:` wrapper on input): - #### forceCollapsingSupply - - Determines whether the tokens in this collection will be minted as **CollapsingSupply** types. This supply type allows the collection owner to mint new tokens as long as the token's circulating supply does not exceed it's max supply. Burning tokens reduces the max supply, meaning that in collapsing supply type, burned tokens cannot be re-minted. + Boolean. If `true`, every token in the collection uses the `COLLAPSING_SUPPLY` cap by default — burned tokens cannot be re-minted. - #### maxTokenCount - - Specifies the maximum number of tokens that can be issued within this collection, setting an upper limit on the total token count. + Optional cap on the number of distinct token ids the collection can hold. - #### maxTokenSupply - - Sets the maximum quantity of each unique token within this collection that can be minted, establishing an upper limit on the available supply for individual tokens. + Optional cap on the supply of each individual token in the collection. ## marketPolicy -Configures default marketplace royalties for all tokens in the collection. +Returned on `Collection.marketPolicy`. Lists the default royalty beneficiaries for marketplace sales of any token in the collection, each with an `accountId` and `percentage`. Set per-collection royalties on `createCollection` via the top-level `royalties` field; per-token royalties via the token's `behavior` field. - #### royalty - - A default royalty percentage for all tokens in the collection. This can be overridden on a per-token basis. + A `{ address, percentage }` pair that will receive a share of every marketplace sale of any token in the collection. Multiple beneficiaries supported. ## explicitRoyaltyCurrencies -The `explicitRoyaltyCurrencies` field within a collection serves the purpose of specifying which currencies are permitted to be accepted as royalties. - -By including specific currencies in this field, a collection restricts the acceptance of royalties to only those currencies, ensuring that any listing created that intends to pay royalties in an unaccepted currency will result in a failed listing creation process. - -This feature enhances control and compliance in managing royalties within the collection. - -## freezeType - -"Freezing" refers to the process of temporarily suspending the transferability of a collection or a specific token. - -The `freezeType` parameter offers flexibility in specifying what can be frozen within the blockchain system, with the following options: - -- "COLLECTION": Indicates that an entire collection of tokens can be frozen. - -- "COLLECTION_ACCOUNT": Allows for the freezing of an entire collection account. - -- "TOKEN": Permits the freezing of individual tokens. - -- "TOKEN_ACCOUNT": Allows for the freezing of an entire token account. - -## markAsProcessing - -The "markAsProcessing" action involves retrieving a list of newly pending transactions and subsequently marking them as "processing," indicating that they are currently being handled or executed within the blockchain system. - -## owner - -The term "owner" can be employed in various contexts, depending on the specific action being performed. - -For example, when unapproving a collection, "owner" refers to the account from which the collection approval will be removed, signifying the entity or account that holds ownership or control over the approval status of the collection. +On `createCollection` / `mutateCollection`, restricts which currency tokens are allowed to be used as royalty payments for tokens in this collection. Pass a list of `{ collectionId, tokenId }` pairs identifying the permitted currency tokens. -# Beam +## infusion -## TokenIds +On `createToken` / `createTokens`, the amount of ENJ to lock inside the token at creation time as backing. Combined with `anyoneCanInfuse`, this controls whether outsiders can add additional ENJ to the token later. -Specifies a list of integer s claimable in the beam. A Beam Claim will be generated for each provided ID. -Provide IDs as strings, using commas to separate multiple entries. Supports single IDs (e.g., "1"), ranges (e.g., "6..8"), or a combination (e.g., ["1", "6..8", "10"] includes IDs 1, 6, 7, 8, and 10). +## anyoneCanInfuse -## tokenQuantityPerClaim +Boolean. On `createToken`, if `true`, any account may add ENJ to the token via `infuseToken` / `infuseTokens`. If `false`, only the collection owner can infuse the token. -Specifies how many token units will be minted or transferred for each Beam Claim. +## removeTokenStorage -## claimQuantity +On `burnToken`, set to `true` to fully remove the token's storage when the burn empties its supply. Set to `false` if you intend to re-mint the token later (only relevant for `SUPPLY`-capped tokens). -Specifies the number of Beam Claims to create for each provided token ID. Defaults to 1. +## groupIds -## type {#beam-claim-type} +On `setTokenGroups`, the full set of group ids the token should belong to after the call. Pass `[]` to remove the token from every group. -Specifies how tokens are delivered when the beam is claimed: +## groupId -- #### TRANSFER_TOKEN +On `addTokenToGroup` / `removeTokenFromGroup`, the id of the token group to add the token to / remove the token from. - - Transfers existing tokens to the recipient. +## groups -- #### MINT_ON_DEMAND +On `createToken` / `createTokens`, an optional list of group ids to add the new token to at creation time. - - Mints tokens directly to the recipient. +## fill -## claimLimit +On `nominationPoolsBond`, set to `true` to bond the signer's entire free ENJ balance into the pool. Pass `amount` instead for a specific bond size. -Specify the number of times each user can claim the beam. +## unbondingPoints -## flags +On `nominationPoolsUnbond`, the number of pool "points" to unbond — pool points are an internal accounting unit, not raw ENJ. See [Nomination Pools](/03-api-reference/02-mutations/01-transaction-mutations.md) for the full bonding model. -Specifies the type of beam behavior to modify. Available options include: +## listingData -- #### PAUSED +On `createListing`, configures the type of listing being created. Shape: - - Temporarily prevents claims from being processed. +```graphql +listingData: { + type: FIXED_PRICE | AUCTION | OFFER + startBlock: # optional + endBlock: # optional + expiration: # optional +} +``` -- #### SINGLE_USE +## usesWhitelist - - Enables the use of single-use claim codes for distributing specific tokens from the beam. +On `createListing`, set to `true` to restrict which accounts can fill the listing. Manage the whitelist with `addWhitelistedAccounts` / `removeWhitelistedAccounts`. -- #### PRUNABLE +## price - - Allows claim records for expired beams to be deleted from the database after a set period. \ No newline at end of file +On `createListing` / `placeBid` / `placeCounterOffer`, the price in the smallest unit of the asset specified by `takeAssetId` (or ENJ if `takeAssetId` is omitted). From b8357aac70e321bb95efc3691b67a5fd4662efee Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 25 May 2026 18:40:25 +0300 Subject: [PATCH 32/67] Rewrite Transactions mutations reference for v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop AcknowledgeEvents / UpdateTransaction / RetryTransactions — none of them exist in v3. Document the actual v3 root-level mutation surface instead: CreateTransaction (with TransactionInput discriminator), CreateBatchTransaction, SignTransaction (external signer flow returning the updated Transaction), and RefreshMetadata. Cross-link CreateManagedWallet to the Wallets page where it belongs. --- .../02-mutations/01-transaction-mutations.md | 143 +++++++++++++++--- 1 file changed, 120 insertions(+), 23 deletions(-) diff --git a/docs/03-api-reference/02-mutations/01-transaction-mutations.md b/docs/03-api-reference/02-mutations/01-transaction-mutations.md index 225cc67..82087fa 100644 --- a/docs/03-api-reference/02-mutations/01-transaction-mutations.md +++ b/docs/03-api-reference/02-mutations/01-transaction-mutations.md @@ -1,7 +1,7 @@ --- title: "Transactions" slug: "transactions" -description: "Use the Enjin API to perform transactions mutations, enabling the creation and modification of blockchain transactions, including asset transfers." +description: "Submit on-chain actions through the Enjin Platform's transaction mutations: CreateTransaction, CreateBatchTransaction, SignTransaction, RefreshMetadata." --- import Tabs from '@theme/Tabs'; @@ -16,19 +16,38 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere `https://platform.beta.enjin.io/graphql` ::: -This is an overview of some of the most commonly used operations in the Enjin Platform Schema. +The Enjin Platform exposes a small number of root-level mutations. Almost every on-chain action — creating a collection, minting, transferring, listing, burning, freezing, bonding to a nomination pool, managing token groups — is funnelled through the single `CreateTransaction` mutation, with the action selected by which field is set on the `transaction` input. -## AcknowledgeEvents +## CreateTransaction -The `AcknowledgeEvents` mutation allows you to confirm the processing of specific events by acknowledging them and removing them from the cache. This helps maintain cache integrity and ensures efficient system operation by eliminating unnecessary data. +Submits a single on-chain action. The `transaction` argument is a `TransactionInput` — set exactly one field on it to choose the action (`createCollection`, `createToken`, `mintToken`, `transferToken`, `transferEnj`, `burnToken`, `freezeToken`, `createListing`, and ~40 more). See [Important Arguments](/03-api-reference/04-important-arguments.md) for the per-field shapes. + +The response is a `Transaction` whose `uuid` you can use to track or sign the transaction later. ```graphql -mutation AcknowledgeEvents{ - AcknowledgeEvents( - uuids: ["25f506ef-92a1-40a9-b034-60f9ab5045da"] - ) +mutation CreateTransaction { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + createToken: { + recipient: "efRecipientAddress" + collectionId: 12345 + tokenId: 1 + initialSupply: 10 + listingForbidden: false + infusion: 0 + anyoneCanInfuse: false + } + } + ) { + uuid + action + state + extrinsicHash + } } ``` @@ -36,25 +55,47 @@ mutation AcknowledgeEvents{ ```json { "data": { - "AcknowledgeEvents": true + "CreateTransaction": { + "uuid": "a90ded41-4262-40a2-95c0-98255b660bf1", + "action": "MultiTokens.create_token", + "state": "PENDING", + "extrinsicHash": null + } } } ```
-## UpdateTransaction +Useful additional arguments: + +- `idempotencyKey: String` — pass the same key on a retried request to avoid creating a duplicate transaction. +- `signerAddress: String` — sign with a specific account instead of the Wallet Daemon (e.g. a Managed Wallet's public key, or an external user address for client-side signing). +- `signerExternalId: String` — same as `signerAddress` but resolved from a Managed Wallet's `externalId`. +- `proxyAddress: String` — wrap the call in `proxy.proxy` so it executes on behalf of a proxied account. +- `fuelTank: String` — dispatch through a fuel tank's address; the tank pays the transaction fees. + +## CreateBatchTransaction -The `UpdateTransaction` mutation enables you to update an existing transaction with new information. This is useful for maintaining accurate transaction records and tracking the lifecycle of transactions. +The batched form of `CreateTransaction`. Pass a list of `TransactionInput` items in `transactions:` and the platform submits them as a single batched extrinsic. ```graphql -mutation UpdateTransaction{ - UpdateTransaction( - id: 7 - signedAtBlock: 320100 - ) +mutation CreateBatchTransaction { + CreateBatchTransaction( + network: ENJIN + chain: MATRIX + transactions: [ + { transferToken: { recipient: "efAlice", collectionId: 12345, tokenId: 1, amount: 5 } } + { transferToken: { recipient: "efBob", collectionId: 12345, tokenId: 1, amount: 3 } } + { transferEnj: { recipient: "efCarol", amount: 1000000000000000000 } } + ] + ) { + uuid + action + state + } } ``` @@ -62,22 +103,41 @@ mutation UpdateTransaction{ ```json { "data": { - "UpdateTransaction": true + "CreateBatchTransaction": { + "uuid": "f3a91d2f-04e0-4d6f-aef9-bbb1cf3220a7", + "action": "Utility.batch_all", + "state": "PENDING" + } } } ```
-## RetryTransactions +`CreateBatchTransaction` accepts the same auxiliary arguments as `CreateTransaction` (`signerAddress`, `signerExternalId`, `idempotencyKey`, `proxyAddress`, `fuelTank`). + +## SignTransaction + +Submits the signed extrinsic for a transaction that was created with an external `signerAddress`. Use this when you want a user's wallet (e.g. through WalletConnect) — rather than the Wallet Daemon — to sign on-chain actions. + +The typical flow is: -The `RetryTransactions` mutation provides a mechanism to retry transactions that have previously failed. This is particularly useful for addressing transaction failures caused by temporary issues such as network congestion or gas price spikes. +1. Call `CreateTransaction(..., signerAddress: )`. The platform returns a `Transaction` with `uuid` and `encodedData` (the unsigned extrinsic). +2. The user's wallet signs `encodedData`, producing a signed extrinsic. +3. Call `SignTransaction(uuid:, signedExtrinsic:)` to submit it. The platform broadcasts the extrinsic and returns the updated `Transaction`. ```graphql -mutation RetryTransactions{ - RetryTransactions(ids: [175]) +mutation SignTransaction { + SignTransaction( + uuid: "a90ded41-4262-40a2-95c0-98255b660bf1" + signedExtrinsic: "0x4502840016eda5..." + ) { + uuid + state + extrinsicHash + } } ``` @@ -85,9 +145,46 @@ mutation RetryTransactions{ ```json { "data": { - "RetryTransactions": true + "SignTransaction": { + "uuid": "a90ded41-4262-40a2-95c0-98255b660bf1", + "state": "BROADCAST", + "extrinsicHash": "0xa1b2c3..." + } } } ```
-
\ No newline at end of file +
+ +## RefreshMetadata + +Forces the platform to re-fetch off-chain metadata for one or more tokens / collections. Useful after you update the JSON behind a token's metadata URI. + +You can target either by canonical token id (`ids: ["12345-1", "12345-2"]`) or by `collectionId` plus a list of `tokenIds`, or by a list of metadata `uris`. + + + +```graphql +mutation RefreshMetadata { + RefreshMetadata( + network: ENJIN + chain: MATRIX + ids: ["12345-1", "12345-2"] + ) +} +``` + + +```json +{ + "data": { + "RefreshMetadata": true + } +} +``` + + + +## CreateManagedWallet + +Creates a new platform-managed wallet, signed automatically by the Wallet Daemon. Covered in detail on the [Wallets Mutations](/03-api-reference/02-mutations/04-wallets-mutations.md) page. From 0b0feb440fb0e992167fccd8b0d4901e36afe3d2 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 25 May 2026 18:41:35 +0300 Subject: [PATCH 33/67] Rewrite Wallets mutations reference for v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CreateWallet → CreateManagedWallet (renamed), and SetWalletAccount / VerifyAccount / UpdateWalletExternalId all no longer exist in v3: managed wallets are keypairs derived deterministically from the daemon seed and the externalId, so neither the account nor the externalId can be changed after the fact, and QR-style account verification has no v3 equivalent. Page collapses to a single CreateManagedWallet section, with cross-links to the signing flows on the Transactions mutations page so readers don't end up here when looking for end-user wallet signing or transaction submission. The #getmanagedwallet anchor link warning is intentional — that anchor lands when 04-wallets-queries.md is migrated. --- .../02-mutations/04-wallets-mutations.md | 102 +++--------------- 1 file changed, 16 insertions(+), 86 deletions(-) diff --git a/docs/03-api-reference/02-mutations/04-wallets-mutations.md b/docs/03-api-reference/02-mutations/04-wallets-mutations.md index 278123d..ae87768 100644 --- a/docs/03-api-reference/02-mutations/04-wallets-mutations.md +++ b/docs/03-api-reference/02-mutations/04-wallets-mutations.md @@ -1,7 +1,7 @@ --- title: "Wallets" slug: "wallets" -description: "Perform wallets mutations in the Enjin API to manage wallet data, transfer assets, and update balances securely on the blockchain." +description: "Create platform-managed wallets via the Enjin API." --- import Tabs from '@theme/Tabs'; @@ -16,73 +16,23 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere `https://platform.beta.enjin.io/graphql` ::: -This is a detailed reference guide that explains the most commonly used operations. +The Enjin Platform has one wallet-related mutation: `CreateManagedWallet`. Managed wallets are keypairs derived from the Wallet Daemon's seed plus an `externalId` you control — they let your application create signing accounts on demand without managing private keys. -## CreateWallet +For end-user wallets connected via WalletConnect, there's no mutation here — the user's wallet already exists on chain. To submit transactions signed by an end-user wallet, see `CreateTransaction(..., signerAddress: )` followed by `SignTransaction(uuid:, signedExtrinsic:)` on the [Transactions](/03-api-reference/02-mutations/01-transaction-mutations.md) page. -The `CreateWallet` mutation in is used to create a new wallet on the blockchain using an external identifier (external ID). This allows you to associate a wallet with an entity on the Enjin Platform. +## CreateManagedWallet - - -```graphql -mutation CreateWallet { - CreateWallet( - externalId: "e73f9f38-6832-4822-922b-b9225245ba24" - ) -} -``` - - -```json -{ - "data": { - "CreateWallet": true - } -} -``` - - - -## SetWalletAccount - -The `SetWalletAccount` mutation is used to associate an existing wallet account on the Enjin Platform with a game developer's external ID. This action links the user's wallet on the Enjin Platform to an external ID stored in the game developer's database, allowing the identification of a player's wallet within the Enjin Platform. - - - -```graphql -mutation SetWalletAccount{ - SetWalletAccount( - externalId: "ZMMP0FCq6NEKphFlNfESdbC81BXWebNHUiguJvFsb5dVnthIvBbFAaNfCE3mOJREDPS5lwWN4sDQXDV61fqRWUKVSeq4ItavScDS" - account: "cxLc8HSeuiLiYmEG7XB8wQrbkRPNCaDi5dVpqPkyhWdEp6i63" - ) -} -``` - - -```json -{ - "data": { - "SetWalletAccount": true - } -} -``` - - +Creates a new managed wallet keyed by your `externalId`. The wallet is the same keypair across networks (mainnet and canary) — there is no `network` / `chain` argument. -## VerifyAccount - -The `VerifyAccount` mutation is used by a wallet to confirm the ownership of a user account. This security measure ensures that actions performed on the account are authorized by the legitimate owner, enhancing the overall security and trustworthiness of the system. +Once created, look up the wallet's public key with [`GetManagedWallet(externalId:)`](/03-api-reference/01-queries/04-wallets-queries.md#getmanagedwallet) and use it as `recipient` on mint/transfer actions, or as `signerAddress` / `signerExternalId` on `CreateTransaction` to have it sign a transaction. ```graphql -mutation VerifyAccount{ - VerifyAccount( - verificationId: "0xfdb976d1c5fc9ed6d546f31332e756ca9c9ef6e3c6f0d9ea0ed0713b1cd84440" - signature: "0x8958f002f9513256fa329fa92441d39adc59e3a19165aeeb343acd72b7f6a" - account: "0xbeac4e574b46a3722da9239f97359b0440c0fb66e3b130f20c2284fd033f9138" - - ) +mutation CreateManagedWallet { + CreateManagedWallet( + externalId: "e73f9f38-6832-4822-922b-b9225245ba24" + ) } ``` @@ -90,35 +40,15 @@ mutation VerifyAccount{ ```json { "data": { - "VerifyAccount": true + "CreateManagedWallet": true } } ```
-## UpdateWalletExternalId - -The `UpdateWalletExternalId` mutation is used to update the external ID associated with a wallet account on the Enjin Platform. This action allows you to change the reference or identifier linked to a user's wallet within the Enjin Platform. - - - -```graphql -mutation UpdateWalletExternalId{ - UpdateWalletExternalId( - id: 8466 - newExternalId: "35b409dc-4f3f-41da-8d94-7635ef2a5853" - ) -} -``` - - -```json -{ - "data": { - "UpdateWalletExternalId": true - } -} -``` - - \ No newline at end of file +:::tip Looking for the signing flow? +- For Wallet Daemon-signed transactions, see [`CreateTransaction`](/03-api-reference/02-mutations/01-transaction-mutations.md#createtransaction). +- For Managed Wallet-signed transactions, pass `signerAddress` or `signerExternalId` on `CreateTransaction`. +- For end-user-signed transactions (WalletConnect, browser extension, etc.), pair `CreateTransaction(... signerAddress:)` with [`SignTransaction`](/03-api-reference/02-mutations/01-transaction-mutations.md#signtransaction). +::: From 392a3f97f450b20990651e9d421f36de0b83eb5e Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 25 May 2026 18:42:48 +0300 Subject: [PATCH 34/67] Rewrite Wallets queries reference for v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GetWallet / GetWallets / GetAccountVerified / RequestAccount / GetPendingWallets are all gone. Replace with the actual v3 read surface: GetAccount and GetAccounts for on-chain accounts (flat balance + flat tokens list, no free/reserved split, no collectionAccounts/tokenAccounts Relay connections), GetManagedWallet and GetManagedWallets (cursor-paginated) for platform-managed wallets, and VerifyMessage for signed-message ownership proofs. Drops the per-token balance / approvals / collectionAccounts sections entirely — those are blocked on dev work (see v3-MIGRATION Open Questions). --- .../01-queries/04-wallets-queries.md | 986 +++--------------- 1 file changed, 118 insertions(+), 868 deletions(-) diff --git a/docs/03-api-reference/01-queries/04-wallets-queries.md b/docs/03-api-reference/01-queries/04-wallets-queries.md index 9776fcb..1a546c0 100644 --- a/docs/03-api-reference/01-queries/04-wallets-queries.md +++ b/docs/03-api-reference/01-queries/04-wallets-queries.md @@ -1,7 +1,7 @@ --- title: "Wallets" slug: "wallets" -description: "Use the Enjin API to perform wallets queries, including retrieving balances, transaction history, and associated blockchain assets." +description: "Read on-chain accounts, managed wallets, and verify signed messages via the Enjin API." --- import Tabs from '@theme/Tabs'; @@ -16,313 +16,42 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere `https://platform.beta.enjin.io/graphql` ::: -This is a detailed reference guide that explains the most commonly used operations. +The Enjin Platform exposes two complementary read surfaces for wallets: -## GetWallet +- **`GetAccount` / `GetAccounts`** — read any on-chain account by address (ENJ balance, held tokens, nonce). +- **`GetManagedWallet` / `GetManagedWallets`** — read platform-managed wallets (the keypairs your application creates via `CreateManagedWallet`). -The `GetWallet` query provides detailed information about a specific wallet. This query retrieves various details related to the wallet, including account information, balances, associated collections and tokens, approvals, and transaction history. +`VerifyMessage` lets you cryptographically verify that a given signature belongs to a given address — useful for signed-message ownership proofs in WalletConnect-style flows. + +## GetAccount + +Returns a single chain account. The response includes the ENJ `balance` (in the smallest unit — divide by 1018 for ENJ) and a flat list of `tokens` the account holds. ```graphql -query GetWallet { - GetWallet( - id: 5 - ) { +query GetAccount { + GetAccount( + network: ENJIN + chain: MATRIX + address: "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y" + ) { + id + address + nonce + balance + tokens { + id + tokenId + isNonFungible + collection { id - account { - publicKey - address - } - externalId - managed - network - nonce - balances { - free - reserved - miscFrozen - feeFrozen - } - collectionAccounts { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - accountCount - isFrozen - collection { - collectionId - } - wallet { - account { - publicKey - address - } - } - approvals { - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - } - tokenAccounts { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - balance - reservedBalance - isFrozen - collection { - collectionId - } - wallet { - account { - publicKey - address - } - } - token { - tokenId - } - approvals { - amount - expiration - wallet { - account { - publicKey - address - } - } - } - namedReserves { - pallet - amount - } - } - } - } - collectionAccountApprovals { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - tokenAccountApprovals { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - amount - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - transactions { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - id - transactionId - transactionHash - method - state - encodedData - events { - edges { - cursor - node { - phase - lookUp - moduleId - eventId - params { - type - value - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } - wallet { - account { - publicKey - address - } - } - } - } - } - ownedCollections { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - network - owner { - account { - publicKey - address - } - } - attributes { - key - value - } - accounts { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - accountCount - isFrozen - collection { - collectionId - } - wallet { - account { - publicKey - address - } - } - approvals { - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - } - tokens { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - tokenId - supply - cap - capSupply - isFrozen - minimumBalance - unitPrice - mintDeposit - attributeCount - collection { - collectionId - } - attributes { - key - value - } - accounts { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - balance - } - } - } - metadata - nonFungible - } - } - } - } - } - } + } + metadata { + name + } } + } } ``` @@ -330,82 +59,20 @@ query GetWallet { ```json { "data": { - "GetWallet": { - "id": 5, - "account": { - "publicKey": "0x1edb67183fcd7846e717b16a37ae4326c7814090c748b5698a3b6a9e946a9f37", - "address": "cxK83gJm3F8LV4Kh1YjA6KaSgJev1RgatHrum4Ap4AFcHSrNe" - }, - "externalId": null, - "managed": false, - "network": "developer", - "nonce": 0, - "balances": { - "free": "0", - "reserved": "0", - "miscFrozen": "0", - "feeFrozen": "0" - }, - "collectionAccounts": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "tokenAccounts": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "collectionAccountApprovals": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "tokenAccountApprovals": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "transactions": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "ownedCollections": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - } + "GetAccount": { + "id": "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y", + "address": "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y", + "nonce": 132, + "balance": "17500000000000000000", + "tokens": [ + { + "id": "3484-2", + "tokenId": "2", + "isNonFungible": false, + "collection": { "id": "3484" }, + "metadata": { "name": "[MvB] Sigil" } + } + ] } } } @@ -413,326 +80,25 @@ query GetWallet {
-## GetWallets +## GetAccounts -The `GetWallets` query allows you to retrieve an array of wallets. You have the option to apply filters based on parameters such as external IDs, verification IDs, or account addresses to narrow down the results. This query is useful for applications that need to access and display user wallet information, including transaction history and asset balances. - -:::warning This query might return items in multiple pages using Connections -To learn how to use GraphQL cursors for pagination, head to [Using the API → Pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination). -::: +The batched form of `GetAccount`. Pass a list of `addresses` and the platform returns an `Account` per entry. ```graphql -query GetWallets { - GetWallets( - ids: [5] +query GetAccounts { + GetAccounts( + network: ENJIN + chain: MATRIX + addresses: [ + "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y" + "efUfmhm2rcTtVr4ajneB71qbfDZ1EkyYW7xRc6fhuRh3Tg4L1" + ] ) { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - id - account { - publicKey - address - } - externalId - managed - network - nonce - balances { - free - reserved - miscFrozen - feeFrozen - } - collectionAccounts { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - accountCount - isFrozen - collection { - collectionId - } - wallet { - account { - publicKey - address - } - } - approvals { - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - } - tokenAccounts { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - balance - reservedBalance - isFrozen - collection { - collectionId - } - wallet { - account { - publicKey - address - } - } - token { - tokenId - } - approvals { - amount - expiration - wallet { - account { - publicKey - address - } - } - } - namedReserves { - pallet - amount - } - } - } - } - collectionAccountApprovals { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - tokenAccountApprovals { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - amount - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - transactions { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - id - transactionId - transactionHash - method - state - encodedData - events { - edges { - cursor - node { - phase - lookUp - moduleId - eventId - params { - type - value - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } - wallet { - account { - publicKey - address - } - } - } - } - } - ownedCollections { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - network - owner { - account { - publicKey - address - } - } - attributes { - key - value - } - accounts { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - accountCount - isFrozen - collection { - collectionId - } - wallet { - account { - publicKey - address - } - } - approvals { - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - } - tokens { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - tokenId - supply - cap - capSupply - isFrozen - minimumBalance - unitPrice - mintDeposit - attributeCount - collection { - collectionId - } - attributes { - key - value - } - accounts { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - balance - } - } - } - metadata - nonFungible - } - } - } - } - } - } - } - } + address + balance + nonce } } ``` @@ -741,121 +107,40 @@ query GetWallets { ```json { "data": { - "GetWallets": { - "totalCount": 1, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false + "GetAccounts": [ + { + "address": "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y", + "balance": "17500000000000000000", + "nonce": 132 }, - "edges": [ - { - "cursor": "eyJpZCI6NSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 5, - "account": { - "publicKey": "0x1edb67183fcd7846e717b16a37ae4326c7814090c748b5698a3b6a9e946a9f37", - "address": "cxK83gJm3F8LV4Kh1YjA6KaSgJev1RgatHrum4Ap4AFcHSrNe" - }, - "externalId": null, - "managed": false, - "network": "developer", - "nonce": 0, - "balances": { - "free": "0", - "reserved": "0", - "miscFrozen": "0", - "feeFrozen": "0" - }, - "collectionAccounts": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "tokenAccounts": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "collectionAccountApprovals": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "tokenAccountApprovals": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "transactions": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "ownedCollections": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - } - } - } - ] - } + { + "address": "efUfmhm2rcTtVr4ajneB71qbfDZ1EkyYW7xRc6fhuRh3Tg4L1", + "balance": "5000000000000000000", + "nonce": 47 + } + ] } } ``` -## GetAccountVerified +## GetManagedWallet -The `GetAccountVerified` query allows you to check the verification status of a specific account. It is used to determine whether an account is verified. The query takes a `verificationId` as a parameter, which serves as a unique identifier for the verification process. -The response will include whether the account is verified (`true` or `false`) and additional account information for reference. +Looks up a platform-managed wallet by either `externalId` (the string you set when calling `CreateManagedWallet`) or `publicKey` (the hex public key). Returns the wallet's public key and externalId — pass either back as `recipient` on mint/transfer actions or as `signerAddress` / `signerExternalId` on `CreateTransaction`. ```graphql -query GetAccountVerified { - GetAccountVerified( - verificationId: "0x684768963b832de4f593da456ed3d099c2b8c580383492d3b0e735c4d1dfe740" - ) { - verified - account { - publicKey - address - } - } +query GetManagedWallet { + GetManagedWallet( + network: ENJIN + chain: MATRIX + externalId: "e73f9f38-6832-4822-922b-b9225245ba24" + ) { + publicKey + externalId + } } ``` @@ -863,12 +148,9 @@ query GetAccountVerified { ```json { "data": { - "GetAccountVerified": { - "verified": true, - "account": { - "publicKey": "0x8b4cd0e710d3cd14b3a407cac1326dc6c8ac68b4ff22f8d239cbd460d831d608", - "address": "cxMaEYKfjJQrQQZ4cmXUjRqfPNRkRUSgsESMiEeyP6pzsBm4V" - } + "GetManagedWallet": { + "publicKey": "0xded3c8a93e4a4b3a5e8f3c9a2d1b6f0e7c8a9b0d1e2f3a4b5c6d7e8f9a0b1c2d", + "externalId": "e73f9f38-6832-4822-922b-b9225245ba24" } } } @@ -876,20 +158,29 @@ query GetAccountVerified {
-## RequestAccount +## GetManagedWallets + +Returns a paginated list of managed wallets. Pagination is cursor-based — pass the previous response's `nextCursor` to fetch the next page; a `null` `nextCursor` means there are no more. -The `RequestAccount` query is designed for generating a QR code that users can scan to provide their wallet account information. It facilitates user authentication and linking wallet accounts to a service or application. The generated QR code contains encoded data that, when scanned, triggers a specific action, such as authentication or the transfer of wallet details. +You can optionally filter to specific `publicKeys` or `externalIds`. ```graphql -query RequestAccount { - RequestAccount( - callback: "https://enjin.io" - ) { - qrCode - verificationId +query GetManagedWallets { + GetManagedWallets( + network: ENJIN + chain: MATRIX + limit: 15 + ) { + data { + publicKey + externalId } + perPage + nextCursor + previousCursor + } } ``` @@ -897,9 +188,20 @@ query RequestAccount { ```json { "data": { - "RequestAccount": { - "qrCode": "https://chart.googleapis.com/chart?chs=512x512&cht=qr&chl=https://deeplink.wallet.enjin.io/proof/0xa144ac5ef19bf4e3523cca27d9f998781c64692ea68843602da0dc95f6c4aa70:A2010LJAJ:aHR0cHM6Ly9lbmppbi5pbw==", - "verificationId": "0xa144ac5ef19bf4e3523cca27d9f998781c64692ea68843602da0dc95f6c4aa70" + "GetManagedWallets": { + "data": [ + { + "publicKey": "0xded3c8a9...", + "externalId": "user-1" + }, + { + "publicKey": "0x84f12b67...", + "externalId": "user-2" + } + ], + "perPage": 15, + "nextCursor": "eyJpZCI6Mn0=", + "previousCursor": null } } } @@ -909,17 +211,18 @@ query RequestAccount { ## VerifyMessage -The `VerifyMessage` query is used to verify the authenticity of a message by confirming that it was signed with a specific private key corresponding to the provided public key. This query is essential for ensuring the integrity and origin of messages in a secure communication or authentication process. +Cryptographically verifies that `signature` was produced by the holder of `publicKey` over `message`. Returns `true` or `false`. Useful for signed-message ownership proofs — for example, prompting a user's wallet to sign a unique challenge string so your backend can confirm they control the address. ```graphql query VerifyMessage { VerifyMessage( - message: "0x74657374" - signature: "0xbe32fc5fbe033ebb2dd94bcf0d06a52299042592006b0956cfd7606dea015b60f834d3b299172ad098e841efa3a61e9fb8e37a8912f85821eaf3df64f0e49083" - publicKey: "0xb0e3d2a901fe662e3f693704a599d4ec06da13b213f46e9ba479ba8e95dbfa61" - cryptoSignatureType: SR25519 + network: ENJIN + chain: MATRIX + publicKey: "0xded3c8a93e4a4b3a5e8f3c9a2d1b6f0e7c8a9b0d1e2f3a4b5c6d7e8f9a0b1c2d" + signature: "0x8958f002f9513256fa329fa92441d39adc59e3a19165aeeb343acd72b7f6a..." + message: "Sign in to MyGame at 2026-05-25T14:32:00Z" ) } ``` @@ -934,56 +237,3 @@ query VerifyMessage { ``` - -## GetPendingWallets - -The `GetPendingWallets` query is used to retrieve an array of wallet accounts that have not yet undergone the verification process. This query is particularly useful for administrators and account management systems to identify and handle pending wallet accounts that require verification. - - - -```graphql -query GetPendingWallets { - GetPendingWallets { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - id - account { - publicKey - address - } - externalId - managed - network - } - } - } -} -``` - - -```json -{ - "data": { - "GetPendingWallets": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - } - } -} -``` - - From e506b347658a0ca3a7fa93c078abaee146b76358 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 25 May 2026 18:44:16 +0300 Subject: [PATCH 35/67] Rewrite Collections queries reference for v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GetCollection and GetCollections are real, but the v2 shapes are gone: Relay edges/node/pageInfo connections, the wallet.account nesting, forceSingleMint, the nested tokens/accounts/approvals connections — none of those exist now. Replace with the actual v3 Collection shape (id/owner/attributes/metadata/mintPolicy/ marketPolicy/transferPolicy/pendingTransfer/tokenGroups), the flat list returned by GetCollections, and add the new GetPendingCollectionTransfers query that pairs with the mutateCollection.owner → acceptCollectionTransfer transfer flow. Drop the v2 "tracked collections" warnings — collections are read publicly in v3, no scoping required. --- .../01-queries/02-collections-queries.md | 456 +++++------------- 1 file changed, 130 insertions(+), 326 deletions(-) diff --git a/docs/03-api-reference/01-queries/02-collections-queries.md b/docs/03-api-reference/01-queries/02-collections-queries.md index b3940b7..5ba6630 100644 --- a/docs/03-api-reference/01-queries/02-collections-queries.md +++ b/docs/03-api-reference/01-queries/02-collections-queries.md @@ -1,7 +1,7 @@ --- title: "Collections" slug: "collections" -description: "Query the Enjin API to retrieve data about collections, including metadata and token information within a collection." +description: "Query the Enjin API for collections: metadata, policies, token groups, and pending transfers." --- import Tabs from '@theme/Tabs'; @@ -16,83 +16,50 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere `https://platform.beta.enjin.io/graphql` ::: -This is a detailed reference guide that explains the most commonly used operations. +`Collection` is the parent resource for a set of related tokens. A collection carries its mint, market, and transfer policies, its on-chain attributes, and any token groups defined within it. Individual tokens within a collection are read separately via [`GetTokens`](/03-api-reference/01-queries/03-tokens-queries.md#gettokens). -## Get Collection +## GetCollection -The `GetCollection` query allows you to retrieve detailed information about a specific collection by providing its `collectionId`. This information includes collection attributes, token details, and associated accounts. - -:::warning Reading Third-Party Collections -Please note that the Enjin Platform Cloud is set up to show only the collections and tokens that were created via the auth-ed Enjin Platform Cloud account.\ -To get a collection that was created elsewhere (via a different Enjin Platform Cloud account / [NFT.io](https://nft.io) / [Enjin Console](https://console.enjin.io) / etc.) the collection must be "Tracked" first, or the query response will return an error.\ -Learn more about tracking a collection in the [Tracking Collections section](/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md#tracking-collections). -::: +Returns a single collection by id. ```graphql query GetCollection { - GetCollection(collectionId: 7153) { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network + GetCollection(network: ENJIN, chain: MATRIX, id: 3484) { + id owner { - account { - publicKey - address - } + address } attributes { key value } - tokens { - edges { - cursor - node { - tokenId - } - } - totalCount - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } + metadata { + name + description + } + mintPolicy { + forceCollapsingSupply + maxTokenCount + maxTokenSupply } - accounts { - edges { - cursor - node { - accountCount - isFrozen - wallet { - account { - publicKey - address - } - } - approvals { - expiration - wallet { - account { - publicKey - address - } - } - } - } + marketPolicy { + beneficiaries { + accountId + percentage } - totalCount - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor + } + transferPolicy { + isFrozen + } + pendingTransfer { + address + } + tokenGroups { + id + metadata { + name } } } @@ -104,93 +71,31 @@ query GetCollection { { "data": { "GetCollection": { - "collectionId": "7153", - "maxTokenCount": 100, - "maxTokenSupply": "100", - "forceSingleMint": false, - "frozen": false, - "network": "canary", + "id": "3484", "owner": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } + "address": "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y" }, "attributes": [], - "tokens": { - "edges": [ - { - "cursor": "eyJpZCI6NzI3MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "tokenId": "10" - } - }, - { - "cursor": "eyJpZCI6NzI3MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "tokenId": "6" - } - }, - { - "cursor": "eyJpZCI6Mzk0MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "tokenId": "68" - } - }, - { - "cursor": "eyJpZCI6Mzk1MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "tokenId": "70468841277235617716769448539773927607" - } - } - ], - "totalCount": 4, - "pageInfo": { - "hasNextPage": false, - "hasPreviousPage": false, - "startCursor": "", - "endCursor": "" - } + "metadata": { + "name": "Multiverse Brotherhood", + "description": null }, - "accounts": { - "edges": [ - { - "cursor": "eyJpZCI6MTM0OCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "accountCount": 2, - "isFrozen": false, - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "approvals": [] - } - }, - { - "cursor": "eyJpZCI6MjQ2MSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "accountCount": 2, - "isFrozen": false, - "wallet": { - "account": { - "publicKey": "0x985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050", - "address": "cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts" - } - }, - "approvals": [] - } - } - ], - "totalCount": 2, - "pageInfo": { - "hasNextPage": false, - "hasPreviousPage": false, - "startCursor": "", - "endCursor": "" - } - } + "mintPolicy": { + "forceCollapsingSupply": false, + "maxTokenCount": null, + "maxTokenSupply": null + }, + "marketPolicy": { + "beneficiaries": [] + }, + "transferPolicy": { + "isFrozen": false + }, + "pendingTransfer": null, + "tokenGroups": [ + { "id": "60", "metadata": { "name": "[MvB] Clan Tag" } }, + { "id": "63", "metadata": { "name": "Multiverse Brotherhood ©" } } + ] } } } @@ -198,93 +103,32 @@ query GetCollection { -## GetCollections +`pendingTransfer` is non-null while a transfer is in progress — see [`GetPendingCollectionTransfers`](#getpendingcollectiontransfers). -:::warning 🚧 Using the Enjin Platform Cloud? -The Enjin Platform Cloud has collections and tokens scoping enabled, to ensure a better experience by only showing you collections and tokens you created using the cloud. -::: +## GetCollections -The `GetCollections` query allows you to retrieve an array of collections. You can optionally filter the collections by providing a list of collection IDs that you are interested in. +Returns a flat list of collections. Pass `ids` to fetch specific collections, or `address` to fetch collections owned by an address (or both to intersect). ```graphql query GetCollections { - GetCollections(collectionIds: [7153]) { - edges { - cursor - node { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - owner { - account { - publicKey - address - } - } - attributes { - key - value - } - tokens { - edges { - cursor - node { - tokenId - } - } - totalCount - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - } - accounts { - edges { - cursor - node { - accountCount - isFrozen - wallet { - account { - publicKey - address - } - } - approvals { - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - totalCount - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - } - } + GetCollections( + network: ENJIN + chain: MATRIX + ids: [3484, 7153] + ) { + id + owner { + address } - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor + metadata { + name + } + mintPolicy { + forceCollapsingSupply + maxTokenCount } - totalCount } } ``` @@ -293,111 +137,71 @@ query GetCollections { ```json { "data": { - "GetCollections": { - "edges": [ - { - "cursor": "eyJjb2xsZWN0aW9uX2lkIjo3MTUzLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "collectionId": "7153", - "maxTokenCount": 100, - "maxTokenSupply": "100", - "forceSingleMint": false, - "frozen": false, - "network": "canary", - "owner": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "attributes": [], - "tokens": { - "edges": [ - { - "cursor": "eyJpZCI6NzI3MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "tokenId": "10" - } - }, - { - "cursor": "eyJpZCI6NzI3MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "tokenId": "6" - } - }, - { - "cursor": "eyJpZCI6Mzk0MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "tokenId": "68" - } - }, - { - "cursor": "eyJpZCI6Mzk1MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "tokenId": "70468841277235617716769448539773927607" - } - } - ], - "totalCount": 4, - "pageInfo": { - "hasNextPage": false, - "hasPreviousPage": false, - "startCursor": "", - "endCursor": "" - } - }, - "accounts": { - "edges": [ - { - "cursor": "eyJpZCI6MTM0OCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "accountCount": 2, - "isFrozen": false, - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "approvals": [] - } - }, - { - "cursor": "eyJpZCI6MjQ2MSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "accountCount": 2, - "isFrozen": false, - "wallet": { - "account": { - "publicKey": "0x985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050", - "address": "cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts" - } - }, - "approvals": [] - } - } - ], - "totalCount": 2, - "pageInfo": { - "hasNextPage": false, - "hasPreviousPage": false, - "startCursor": "", - "endCursor": "" - } - } - } - } - ], - "pageInfo": { - "hasNextPage": false, - "hasPreviousPage": false, - "startCursor": "", - "endCursor": "" + "GetCollections": [ + { + "id": "3484", + "owner": { "address": "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y" }, + "metadata": { "name": "Multiverse Brotherhood" }, + "mintPolicy": { "forceCollapsingSupply": false, "maxTokenCount": null } }, - "totalCount": 1 + { + "id": "7153", + "owner": { "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" }, + "metadata": { "name": "Test Collection" }, + "mintPolicy": { "forceCollapsingSupply": false, "maxTokenCount": 100 } + } + ] + } +} +``` + + + +## GetPendingCollectionTransfers + +Returns collections that have a pending ownership transfer — the `owner` field on `mutateCollection` was set to a new address, but the recipient hasn't called `acceptCollectionTransfer` yet (and the current owner hasn't called `cancelCollectionTransfer`). + +You can filter by `ids` (specific collections) or `address` (collections being transferred *to* this address). + + + +```graphql +query GetPendingCollectionTransfers { + GetPendingCollectionTransfers( + network: ENJIN + chain: MATRIX + address: "efRecipientWalletAddress" + ) { + id + owner { + address + } + pendingTransfer { + address + } + metadata { + name } } } ``` - \ No newline at end of file + +```json +{ + "data": { + "GetPendingCollectionTransfers": [ + { + "id": "12345", + "owner": { "address": "efOriginalOwnerAddress" }, + "pendingTransfer": { "address": "efRecipientWalletAddress" }, + "metadata": { "name": "Hand-off Collection" } + } + ] + } +} +``` + + + +See the [Transfer a Collection](/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md) guide for the full transfer / accept / cancel flow. From 41408f6e5bc155246ea54bce8b16edba34bd3e83 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 25 May 2026 18:45:09 +0300 Subject: [PATCH 36/67] Rewrite Tokens queries reference for v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GetToken and GetTokens still exist, but tokenId is now a flat BigInt (no {integer:N} wrapper), there are no Relay edges/node/pageInfo connections, no nested accounts/approvals/namedReserves connections, and several fields the v2 examples select don't exist on v3 Token (minimumBalance, unitPrice, mintDeposit, attributeCount, capSupply, reservedBalance). Replace with the actual v3 Token shape (id / tokenId / supply / isFrozen / isNonFungible / isListingForbidden / infusion / anyoneCanInfuse / cap{type,supply} / behavior / attributes / metadata / collection / tokenGroupTokens), offset-based pagination on GetTokens (limit + page), and the optional tokenIds filter. Drop the v2 "tracked collections" warning — collections are read publicly in v3. --- .../01-queries/03-tokens-queries.md | 341 ++++++------------ 1 file changed, 106 insertions(+), 235 deletions(-) diff --git a/docs/03-api-reference/01-queries/03-tokens-queries.md b/docs/03-api-reference/01-queries/03-tokens-queries.md index 2a41750..82ea0d3 100644 --- a/docs/03-api-reference/01-queries/03-tokens-queries.md +++ b/docs/03-api-reference/01-queries/03-tokens-queries.md @@ -1,7 +1,7 @@ --- title: "Tokens" slug: "tokens" -description: "Query the Enjin API to retrieve detailed information about blockchain tokens, including ownership, metadata, and transaction history." +description: "Query the Enjin API for tokens: supply, cap, behavior, attributes, metadata, and token-group membership." --- import Tabs from '@theme/Tabs'; @@ -16,82 +16,55 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere `https://platform.beta.enjin.io/graphql` ::: -This is a detailed reference guide that explains the most commonly used operations. +A `Token` is an individual asset (fungible or non-fungible) inside a [collection](/03-api-reference/01-queries/02-collections-queries.md). Tokens carry their supply, cap, attributes, metadata, behavior (currency / royalty), and any token-group memberships. -## Get Token +## GetToken -The `GetToken` query enables you to retrieve detailed information about a specific token from a collection. It requires the collectionId and tokenId parameters to access attributes, account details, and metadata related to the token. - -You can utilize the GetToken query for various purposes, including: - -- Retrieving Token Information: Get detailed information about a specific token, including supply, cap, and freeze status. -- Exploring Accounts: Explore accounts that hold the token, check their balances, and freeze status. -- Context of Collection: Obtain context about the collection to which the token belongs. -- Retrieving Attributes: Access attributes associated with the collection. -- Metadata Retrieval: Retrieve metadata associated with the token, such as name, color, or other descriptive information. +Returns a single token by either its canonical `id` (`"-"`) or by `collectionId` + `tokenId`. ```graphql query GetToken { GetToken( + network: ENJIN + chain: MATRIX collectionId: 7153 - tokenId: {integer:10} + tokenId: 10 ) { + id tokenId supply - cap - capSupply isFrozen - minimumBalance - unitPrice - mintDeposit - attributeCount - nonFungible - metadata - collection { - collectionId + isNonFungible + isListingForbidden + infusion + anyoneCanInfuse + cap { + type + supply + } + behavior { + __typename + beneficiaries { + accountId + percentage + } } attributes { key value } - accounts { - totalCount - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - edges { - cursor - node { - balance - reservedBalance - isFrozen - wallet { - account { - publicKey - address - } - } - approvals { - amount - expiration - wallet { - account { - publicKey - address - } - } - } - namedReserves { - pallet - amount - } - } - } + metadata { + name + description + } + collection { + id + } + tokenGroupTokens { + tokenGroupId + position } } } @@ -102,48 +75,30 @@ query GetToken { { "data": { "GetToken": { + "id": "7153-10", "tokenId": "10", "supply": "1", - "cap": "SINGLE_MINT", - "capSupply": null, "isFrozen": false, - "minimumBalance": "1", - "unitPrice": "10000000000000000", - "mintDeposit": "20000000000000000", - "attributeCount": 0, - "nonFungible": true, - "metadata": null, - "collection": { - "collectionId": "7153" + "isNonFungible": true, + "isListingForbidden": false, + "infusion": "0", + "anyoneCanInfuse": false, + "cap": { + "type": "SUPPLY", + "supply": "1" }, - "attributes": [], - "accounts": { - "totalCount": 1, - "pageInfo": { - "hasNextPage": false, - "hasPreviousPage": false, - "startCursor": "", - "endCursor": "" - }, - "edges": [ - { - "cursor": "eyJpZCI6NzgxOCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "balance": "1", - "reservedBalance": "0", - "isFrozen": false, - "wallet": { - "account": { - "publicKey": "0x985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050", - "address": "cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts" - } - }, - "approvals": [], - "namedReserves": [] - } - } - ] - } + "behavior": null, + "attributes": [ + { "key": "uri", "value": "https://example.com/metadata/10.json" } + ], + "metadata": { + "name": "Legendary Sword #10", + "description": null + }, + "collection": { "id": "7153" }, + "tokenGroupTokens": [ + { "tokenGroupId": "60", "position": 0 } + ] } } } @@ -151,98 +106,33 @@ query GetToken { -## GetTokens - -The `GetTokens` query allows you to retrieve an array of token data from a collection, making it suitable for batch retrieval and data analysis. You can specify filtering criteria to target specific tokens within the collection. +`Token.behavior.__typename` distinguishes currency tokens (`IS_CURRENCY`) from royalty-bearing tokens (`HAS_ROYALTY`); for plain tokens with no special behavior the whole field is `null`. -You can utilize the `GetTokens` query for various purposes, including: - -- **Bulk Token Retrieval**: Retrieve information about multiple tokens within a collection, making it efficient for displaying token lists or galleries. -- **Filtered Token Retrieval**: Apply filters to obtain specific tokens based on criteria like token ID, collection, or attributes. -- **Marketplace Integration**: Fetch token data for use in a marketplace or trading platform to display available tokens and their details. -- **Data Analysis**: Analyze token data for insights, statistics, or reporting across a collection. -- **User Experience Enhancement**: Enhance the user experience by providing a comprehensive view of tokens within a collection. +## GetTokens - -:::warning Reading Third-Party Tokens -Please note that the Enjin Platform Cloud is set up to show only the collections and tokens that were created via the auth-ed Enjin Platform Cloud account.\ -To get a token that was created elsewhere (via a different Enjin Platform Cloud account / [NFT.io](https://nft.io) / [Enjin Console](https://console.enjin.io) / etc.) the collection must be "Tracked" first, or the query response will return an error.\ -Learn more about tracking a collection in the [Tracking Collections section](/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md#tracking-collections). -::: +Returns a paginated list of tokens scoped to a single `collectionId`. Optionally filter to specific `tokenIds`. Pagination is offset-based — increment `page` until the returned list is empty. ```graphql query GetTokens { GetTokens( + network: ENJIN + chain: MATRIX collectionId: 7153 - tokenIds: [{integer:10}] + limit: 15 + page: 1 ) { - edges { - cursor - node { - tokenId - supply - cap - capSupply - isFrozen - minimumBalance - unitPrice - mintDeposit - attributeCount - collection { - collectionId - } - attributes { - key - value - } - accounts { - totalCount - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - edges { - cursor - node { - balance - reservedBalance - isFrozen - wallet { - account { - publicKey - address - } - } - approvals { - amount - expiration - wallet { - account { - publicKey - address - } - } - } - namedReserves { - pallet - amount - } - } - } - } - nonFungible - } + id + tokenId + supply + isNonFungible + cap { + type + supply } - totalCount - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor + metadata { + name } } } @@ -252,65 +142,46 @@ query GetTokens { ```json { "data": { - "GetTokens": { - "edges": [ - { - "cursor": "eyJjb2xsZWN0aW9uX2lkIjo0OTk4LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "tokenId": "10", - "supply": "1", - "cap": "SINGLE_MINT", - "capSupply": null, - "isFrozen": false, - "minimumBalance": "1", - "unitPrice": "10000000000000000", - "mintDeposit": "20000000000000000", - "attributeCount": 0, - "collection": { - "collectionId": "7153" - }, - "attributes": [], - "accounts": { - "totalCount": 1, - "pageInfo": { - "hasNextPage": false, - "hasPreviousPage": false, - "startCursor": "", - "endCursor": "" - }, - "edges": [ - { - "cursor": "eyJpZCI6NzgxOCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "balance": "1", - "reservedBalance": "0", - "isFrozen": false, - "wallet": { - "account": { - "publicKey": "0x985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050", - "address": "cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts" - } - }, - "approvals": [], - "namedReserves": [] - } - } - ] - }, - "nonFungible": true - } - } - ], - "totalCount": 1, - "pageInfo": { - "hasNextPage": false, - "hasPreviousPage": false, - "startCursor": "", - "endCursor": "" + "GetTokens": [ + { + "id": "7153-6", + "tokenId": "6", + "supply": "1", + "isNonFungible": true, + "cap": { "type": "SUPPLY", "supply": "1" }, + "metadata": { "name": "Legendary Sword #6" } + }, + { + "id": "7153-10", + "tokenId": "10", + "supply": "1", + "isNonFungible": true, + "cap": { "type": "SUPPLY", "supply": "1" }, + "metadata": { "name": "Legendary Sword #10" } } - } + ] } } ``` - \ No newline at end of file +
+ +### Filtering to specific token ids + +Pass `tokenIds: [...]` to restrict the result to a known set: + +```graphql +query GetTokens { + GetTokens( + network: ENJIN + chain: MATRIX + collectionId: 7153 + tokenIds: [6, 10] + limit: 15 + page: 1 + ) { + tokenId + supply + } +} +``` From 334dd2824fa9894676e314cf1d94c9344fc40fed Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 25 May 2026 18:46:29 +0300 Subject: [PATCH 37/67] Rewrite Transactions queries reference for v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop GetPendingEvents entirely (it's gone in v3). Rewrite the remaining queries around the new Transaction shape (uuid not id, action not method, no transactionId/signedAtBlock/wallet.account nesting, no events connection), cursor-paginated GetTransactions, and the new lookup-by-uuid/idempotencyKey/extrinsicHash arguments. Add GetBlock / GetExtrinsic / GetExtrinsics (previously undocumented in this reference) with prominent callouts noting they currently return 500s — readers are pointed to the Subscan workaround via the platform Transactions dashboard until the platform fix lands. --- .../01-queries/01-transactions-queries.md | 1184 +++-------------- 1 file changed, 181 insertions(+), 1003 deletions(-) diff --git a/docs/03-api-reference/01-queries/01-transactions-queries.md b/docs/03-api-reference/01-queries/01-transactions-queries.md index 8a4b3d0..f729993 100644 --- a/docs/03-api-reference/01-queries/01-transactions-queries.md +++ b/docs/03-api-reference/01-queries/01-transactions-queries.md @@ -1,7 +1,7 @@ --- title: "Transactions" slug: "transactions" -description: "Use the Enjin API to query transaction data, including blockchain transactions history, status, and details." +description: "Query the Enjin API for transactions, blocks, and extrinsics." --- import GlossaryTerm from '@site/src/components/GlossaryTerm'; @@ -17,35 +17,78 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere `https://platform.beta.enjin.io/graphql` ::: -This is an overview of some of the most commonly used operations in the Enjin Platform Schema. +Use these queries to look up the state of transactions you've submitted (`GetTransaction` / `GetTransactions`) and to read the underlying blocks and extrinsics they ended up in (`GetBlock` / `GetBlocks`, `GetExtrinsic` / `GetExtrinsics`). -## GetBlocks +## GetTransaction -The `GetBlocks` query allows you to retrieve an array of blocks(), which can be filtered based on specific block numbers, transaction IDs, or hashes. It is designed to fetch detailed data on a set of blocks, providing valuable insights. +Returns a single transaction by `uuid`, `idempotencyKey`, or `extrinsicHash` — pass exactly one. The most common use is to look up the live state of a transaction you submitted via [`CreateTransaction`](/03-api-reference/02-mutations/01-transaction-mutations.md#createtransaction). ```graphql -query GetBlocks { - GetBlocks(numbers: ["402865"]) { - totalCount - pageInfo { - startCursor - endCursor - hasPreviousPage - hasNextPage +query GetTransaction { + GetTransaction( + network: ENJIN + chain: MATRIX + uuid: "a90ded41-4262-40a2-95c0-98255b660bf1" + ) { + uuid + idempotencyKey + action + state + encodedData + extrinsicHash + createdAt + updatedAt + } +} +``` + + +```json +{ + "data": { + "GetTransaction": { + "uuid": "a90ded41-4262-40a2-95c0-98255b660bf1", + "idempotencyKey": "61f0b8e1-e0b9-4ecd-b445-84dc4cd2b495", + "action": "MultiTokens.create_collection", + "state": "FINALIZED", + "encodedData": "0x280dc56f04985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050...", + "extrinsicHash": "0xbafe459e8248b802f3aef98d2e4a695bbb238899edf40519b082366e3ff8b98f", + "createdAt": "2026-05-13T05:20:52+00:00", + "updatedAt": "2026-05-13T05:21:26+00:00" } - edges { - cursor - node { - id - number - hash - synced - failed - exception - } + } +} +``` + + + +See [the state argument](/03-api-reference/04-important-arguments.md#state) for the full list of transaction states. + +## GetTransactions + +Returns a paginated list of transactions. Pagination is cursor-based — pass the previous response's `nextCursor` to fetch the next page. You can filter by `uuids`, `idempotencyKeys`, or `extrinsicHashes`. + + + +```graphql +query GetTransactions { + GetTransactions( + network: ENJIN + chain: MATRIX + limit: 15 + ) { + data { + uuid + action + state + extrinsicHash + createdAt } + perPage + nextCursor + previousCursor } } ``` @@ -54,27 +97,26 @@ query GetBlocks { ```json { "data": { - "GetBlocks": { - "totalCount": 1, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasPreviousPage": false, - "hasNextPage": false - }, - "edges": [ + "GetTransactions": { + "data": [ + { + "uuid": "a90ded41-4262-40a2-95c0-98255b660bf1", + "action": "MultiTokens.create_collection", + "state": "FINALIZED", + "extrinsicHash": "0xbafe459e...", + "createdAt": "2026-05-13T05:20:52+00:00" + }, { - "cursor": "eyJudW1iZXIiOjQwMjg2NSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 51507, - "number": 402865, - "hash": "0xf0b3cee1c36a99e24aaef7da06aec2ecd79a599c19ad4ae6fb4b40b3e497a322", - "synced": true, - "failed": false, - "exception": null - } + "uuid": "b80aed52-3171-50b3-a4d0-87366b771b02", + "action": "MultiTokens.mint", + "state": "BROADCAST", + "extrinsicHash": "0x12ab34cd...", + "createdAt": "2026-05-13T05:24:11+00:00" } - ] + ], + "perPage": 15, + "nextCursor": "eyJpZCI6MTUwfQ==", + "previousCursor": null } } } @@ -82,57 +124,26 @@ query GetBlocks { -## GetTransaction +## GetBlock -The `GetTransaction` query allows you to retrieve detailed information about a specific transaction. It provides comprehensive details about the transaction, including its state, associated wallet, and related events. +Returns a single block by `id` (block number) or `hash`. ```graphql -query GetTransaction { - GetTransaction( - id: 11300 - ) { - id - idempotencyKey - transactionId - transactionHash - method - state - result - encodedData - signedAtBlock - createdAt - updatedAt - wallet { - account { - publicKey - address - } - } - events { - edges { - cursor - node { - phase - lookUp - moduleId - eventId - params { - type - value - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } +query GetBlock { + GetBlock( + network: ENJIN + chain: MATRIX + id: 402865 + ) { + number + hash + validator { + address } + createdAt + } } ``` @@ -140,215 +151,11 @@ query GetTransaction { ```json { "data": { - "GetTransaction": { - "id": 11300, - "idempotencyKey": "61f0b8e1-e0b9-4ecd-b445-84dc4cd2b495", - "transactionId": "402811-2", - "transactionHash": "0xbafe459e8248b802f3aef98d2e4a695bbb238899edf40519b082366e3ff8b98f", - "method": "BatchMint", - "state": "FINALIZED", - "result": "EXTRINSIC_SUCCESS", - "encodedData": "0x280dc56f04985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d05000180400010000c16ff2862300000000000000000001000000000000", - "signedAtBlock": 402809, - "createdAt": "2023-06-23T05:20:52+00:00", - "updatedAt": "2023-06-23T05:21:26+00:00", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "events": { - "edges": [ - { - "cursor": "eyJpZCI6Njk3MTEsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "Balances", - "eventId": "Withdraw", - "params": [ - { - "type": "who", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "amount", - "value": "19981763653182275" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTIsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "Balances", - "eventId": "Reserved", - "params": [ - { - "type": "who", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "amount", - "value": "10000000000000000" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTMsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "MultiTokens", - "eventId": "TokenCreated", - "params": [ - { - "type": "collection_id", - "value": "7153" - }, - { - "type": "token_id", - "value": "6" - }, - { - "type": "issuer", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "initial_supply", - "value": "1" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTQsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "MultiTokens", - "eventId": "TokenAccountCreated", - "params": [ - { - "type": "collection_id", - "value": "7153" - }, - { - "type": "token_id", - "value": "6" - }, - { - "type": "account", - "value": "985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050" - }, - { - "type": "balance", - "value": "1" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "MultiTokens", - "eventId": "Minted", - "params": [ - { - "type": "collection_id", - "value": "7153" - }, - { - "type": "token_id", - "value": "6" - }, - { - "type": "issuer", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "recipient", - "value": "985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050" - }, - { - "type": "amount", - "value": "1" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTYsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "Balances", - "eventId": "Deposit", - "params": [ - { - "type": "who", - "value": "6d6f646c65662f66656469730000000000000000000000000000000000000000" - }, - { - "type": "amount", - "value": "19981763653182275" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTcsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "TransactionPayment", - "eventId": "TransactionFeePaid", - "params": [ - { - "type": "who", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "actual_fee", - "value": "19981763653182275" - }, - { - "type": "tip", - "value": "0" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTgsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "System", - "eventId": "ExtrinsicSuccess", - "params": [ - { - "type": "dispatch_info", - "value": "{\"weight\":{\"ref_time\":\"848559950\",\"proof_size\":\"10283\"},\"class\":\"Normal\",\"pays_fee\":\"Yes\"}" - } - ] - } - } - ], - "totalCount": 8, - "pageInfo": { - "hasPreviousPage": false, - "hasNextPage": false, - "startCursor": "", - "endCursor": "" - } - } + "GetBlock": { + "number": 402865, + "hash": "0xf0b3cee1c36a99e24aaef7da06aec2ecd79a599c19ad4ae6fb4b40b3e497a322", + "validator": { "address": "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y" }, + "createdAt": "2026-05-13T05:20:48+00:00" } } } @@ -356,69 +163,27 @@ query GetTransaction { -## GetTransactions +:::warning GetBlock is currently unavailable +`GetBlock` is temporarily disabled — every call returns an `Internal server error`. For event-level lookups use the [Transactions page on the platform dashboard](https://platform.beta.enjin.io/transactions) and click the **Extrinsic Hash** link on a transaction's row, which opens its events on Subscan. The platform team is aware of the issue and a fix is planned. +::: -The `GetTransactions` query in allows you to retrieve an array of transactions. You can optionally filter these transactions based on various parameters such as transaction IDs, transaction hashes, methods, states, results, or accounts. +## GetBlocks + +Returns a list of blocks. Filter by `ids` (block numbers) or `hashes`. ```graphql -query GetTransactions { - GetTransactions( - ids: [11300] - ) { - edges { - cursor - node { - id - idempotencyKey - transactionId - transactionHash - method - state - result - encodedData - signedAtBlock - createdAt - updatedAt - wallet { - account { - publicKey - address - } - } - events { - edges { - cursor - node { - phase - lookUp - moduleId - eventId - params { - type - value - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } - } - } - totalCount - pageInfo { - startCursor - endCursor - hasPreviousPage - hasNextPage - } - } +query GetBlocks { + GetBlocks( + network: ENJIN + chain: MATRIX + ids: [402865, 402866] + ) { + number + hash + createdAt + } } ``` @@ -426,264 +191,52 @@ query GetTransactions { ```json { "data": { - "GetTransactions": { - "edges": [ - { - "cursor": "eyJpZCI6MTEzMDAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "id": 11300, - "idempotencyKey": "61f0b8e1-e0b9-4ecd-b445-84dc4cd2b495", - "transactionId": "402811-2", - "transactionHash": "0xbafe459e8248b802f3aef98d2e4a695bbb238899edf40519b082366e3ff8b98f", - "method": "BatchMint", - "state": "FINALIZED", - "result": "EXTRINSIC_SUCCESS", - "encodedData": "0x280dc56f04985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d05000180400010000c16ff2862300000000000000000001000000000000", - "signedAtBlock": 402809, - "createdAt": "2023-06-23T05:20:52+00:00", - "updatedAt": "2023-06-23T05:21:26+00:00", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "events": { - "edges": [ - { - "cursor": "eyJpZCI6Njk3MTEsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "Balances", - "eventId": "Withdraw", - "params": [ - { - "type": "who", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "amount", - "value": "19981763653182275" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTIsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "Balances", - "eventId": "Reserved", - "params": [ - { - "type": "who", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "amount", - "value": "10000000000000000" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTMsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "MultiTokens", - "eventId": "TokenCreated", - "params": [ - { - "type": "collection_id", - "value": "7153" - }, - { - "type": "token_id", - "value": "6" - }, - { - "type": "issuer", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "initial_supply", - "value": "1" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTQsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "MultiTokens", - "eventId": "TokenAccountCreated", - "params": [ - { - "type": "collection_id", - "value": "7153" - }, - { - "type": "token_id", - "value": "6" - }, - { - "type": "account", - "value": "985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050" - }, - { - "type": "balance", - "value": "1" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "MultiTokens", - "eventId": "Minted", - "params": [ - { - "type": "collection_id", - "value": "7153" - }, - { - "type": "token_id", - "value": "6" - }, - { - "type": "issuer", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "recipient", - "value": "985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050" - }, - { - "type": "amount", - "value": "1" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTYsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "Balances", - "eventId": "Deposit", - "params": [ - { - "type": "who", - "value": "6d6f646c65662f66656469730000000000000000000000000000000000000000" - }, - { - "type": "amount", - "value": "19981763653182275" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTcsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "TransactionPayment", - "eventId": "TransactionFeePaid", - "params": [ - { - "type": "who", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "actual_fee", - "value": "19981763653182275" - }, - { - "type": "tip", - "value": "0" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTgsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "System", - "eventId": "ExtrinsicSuccess", - "params": [ - { - "type": "dispatch_info", - "value": "{\"weight\":{\"ref_time\":\"848559950\",\"proof_size\":\"10283\"},\"class\":\"Normal\",\"pays_fee\":\"Yes\"}" - } - ] - } - } - ], - "totalCount": 8, - "pageInfo": { - "hasPreviousPage": false, - "hasNextPage": false, - "startCursor": "", - "endCursor": "" - } - } - } - } - ], - "totalCount": 1, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasPreviousPage": false, - "hasNextPage": false + "GetBlocks": [ + { + "number": 402865, + "hash": "0xf0b3cee1c36a99e24aaef7da06aec2ecd79a599c19ad4ae6fb4b40b3e497a322", + "createdAt": "2026-05-13T05:20:48+00:00" + }, + { + "number": 402866, + "hash": "0xa1b2c3d4e5...", + "createdAt": "2026-05-13T05:20:54+00:00" } - } + ] } } ``` -## GetPendingEvents +:::warning Block.extrinsics and Block.events return null +The `extrinsics` and `events` sub-fields on `Block` are defined in the schema but currently return `null` on every request. For now, use the [Transactions dashboard](https://platform.beta.enjin.io/transactions) → **Extrinsic Hash** → Subscan flow for event-level data. +::: + +## GetExtrinsic -The `GetPendingEvents` query is designed to retrieve a list of events that have been broadcast by the system but not yet acknowledged by the client. This is useful for ensuring that no events are missed or unprocessed, particularly in systems that rely on event-driven architectures or asynchronous processing. +Returns a single extrinsic by `hash`. ```graphql -query GetPendingEvents{ - GetPendingEvents( - acknowledgeEvents: false +query GetExtrinsic { + GetExtrinsic( + network: ENJIN + chain: MATRIX + hash: "0xbafe459e8248b802f3aef98d2e4a695bbb238899edf40519b082366e3ff8b98f" ) { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage + id + hash + pallet + method + success + nonce + signer { + address } - edges { - cursor - node { - id - uuid - name - sent - channels - data - } + block { + number } } } @@ -693,385 +246,15 @@ query GetPendingEvents{ ```json { "data": { - "GetPendingEvents": { - "totalCount": 95151, - "pageInfo": { - "startCursor": "", - "endCursor": "eyJpZCI6NjA3LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "hasNextPage": true, - "hasPreviousPage": false - }, - "edges": [ - { - "cursor": "eyJpZCI6NTkzLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 593, - "uuid": "b7678fd9-8447-453b-8821-172d6b84397b", - "name": "platform:beam-claims-complete", - "sent": "2023-05-28 15:08:07", - "channels": [ - "collection;101729", - "platform", - "beam;platform" - ], - "data": { - "id": 11, - "beam": { - "id": 2, - "end": "2028-11-14 00:00:00", - "code": "aa9c8deb3549e635ef2fc40a7285596a", - "name": "373", - "image": "https://en.wikipedia.org/wiki/Firefox#/media/File:Firefox_logo,_2019.svg", - "start": "2022-11-14 00:00:00", - "flags_mask": 0, - "description": "This is the most awesome Beam ever.", - "collection_chain_id": "101729" - }, - "type": "MINT_ON_DEMAND", - "state": "COMPLETED", - "quantity": 10, - "attributes": [], - "claimed_at": "2023-05-28 15:07:37", - "ip_address": "172.69.58.64", - "token_chain_id": "1283", - "wallet_public_key": "0x83800dc53431f36f0aaaad573eded3394c2c976d28d528111f82968f0a601bc2" - } - } - }, - { - "cursor": "eyJpZCI6NTk0LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 594, - "uuid": "50859a2b-574e-4d0d-8577-e8e53f9eee1d", - "name": "platform:transaction-created", - "sent": "2023-05-28 15:08:10", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 98, - "state": "PENDING", - "method": "CreateCollection", - "idempotencyKey": "575992c4-911a-4d59-8ccf-7958d46e605b" - } - } - }, - { - "cursor": "eyJpZCI6NTk1LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 595, - "uuid": "63c4c28d-463c-4455-bb9b-43ccbf89b53a", - "name": "platform:transaction-created", - "sent": "2023-05-28 15:08:10", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 99, - "state": "PENDING", - "method": "BatchSetAttribute", - "idempotencyKey": "0134b2af-b92d-4999-8900-3f8124172247" - } - } - }, - { - "cursor": "eyJpZCI6NTk2LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 596, - "uuid": "fe37740b-cb5b-4605-940a-d2b6927ca2d6", - "name": "platform:beam-claim-pending", - "sent": "2023-05-28 15:08:13", - "channels": [ - "collection;101729", - "platform", - "beam;platform" - ], - "data": { - "beam": { - "id": 2, - "end": "2028-11-14 00:00:00", - "code": "aa9c8deb3549e635ef2fc40a7285596a", - "name": "373", - "image": "https://en.wikipedia.org/wiki/Firefox#/media/File:Firefox_logo,_2019.svg", - "start": "2022-11-14 00:00:00", - "flags_mask": 0, - "description": "This is the most awesome Beam ever.", - "collection_chain_id": "101729" - }, - "state": "PENDING", - "extras": [], - "beam_id": 2, - "claimed_at": "2023-05-28T15:08:13.679083Z", - "ip_address": "172.70.126.55", - "idempotency_key": "0b193cff-8c97-4a72-881b-1adb4432da1d", - "wallet_public_key": "0x66f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f646009" - } - } - }, - { - "cursor": "eyJpZCI6NTk3LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 597, - "uuid": "b3923e2b-3f46-4166-951d-d074c05255a1", - "name": "platform:transaction-updated", - "sent": "2023-05-28 15:08:16", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 99, - "state": "PROCESSING", - "method": "BatchSetAttribute", - "result": null, - "transactionId": null, - "idempotencyKey": "0134b2af-b92d-4999-8900-3f8124172247", - "transactionHash": null - } - } - }, - { - "cursor": "eyJpZCI6NTk4LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 598, - "uuid": "9f4911be-3b0e-4604-8248-01db2f6c9255", - "name": "platform:transaction-updated", - "sent": "2023-05-28 15:08:16", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 98, - "state": "PROCESSING", - "method": "CreateCollection", - "result": null, - "transactionId": null, - "idempotencyKey": "575992c4-911a-4d59-8ccf-7958d46e605b", - "transactionHash": null - } - } - }, - { - "cursor": "eyJpZCI6NTk5LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 599, - "uuid": "b6cfd3bc-5850-415b-9db6-06f7ebea6f50", - "name": "platform:transaction-created", - "sent": "2023-05-28 15:08:16", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 100, - "state": "PENDING", - "method": "BatchMint", - "idempotencyKey": "b09251c5-7911-4d0c-ac02-89ab049aa713" - } - } - }, - { - "cursor": "eyJpZCI6NjAwLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 600, - "uuid": "2de8a2b2-10e7-4769-a432-53c6258e8721", - "name": "platform:beam-claim-in-progress", - "sent": "2023-05-28 15:08:16", - "channels": [ - "collection;101729", - "platform", - "beam;platform" - ], - "data": { - "id": 20, - "beam": { - "id": 2, - "end": "2028-11-14 00:00:00", - "code": "aa9c8deb3549e635ef2fc40a7285596a", - "name": "373", - "image": "https://en.wikipedia.org/wiki/Firefox#/media/File:Firefox_logo,_2019.svg", - "start": "2022-11-14 00:00:00", - "flags_mask": 0, - "description": "This is the most awesome Beam ever.", - "collection_chain_id": "101729" - }, - "quantity": 10, - "attributes": [], - "collection": { - "id": 98684, - "owner": { - "id": 2, - "address": "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", - "managed": 0, - "network": "developer", - "created_at": "2023-05-28T14:47:39.000000Z", - "public_key": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "updated_at": "2023-05-28T14:47:39.000000Z", - "external_id": null, - "verification_id": null - }, - "network": "developer", - "is_frozen": 0, - "created_at": "2023-05-28T15:06:30.000000Z", - "updated_at": "2023-05-28T15:06:30.000000Z", - "token_count": "0", - "total_deposit": "25000000000000000000", - "attribute_count": "0", - "max_token_count": "100", - "owner_wallet_id": 2, - "max_token_supply": "100", - "force_single_mint": 0, - "royalty_wallet_id": null, - "royalty_percentage": null, - "collection_chain_id": "101729" - }, - "token_chain_id": "1283", - "wallet_public_key": "0x66f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f646009" - } - } - }, - { - "cursor": "eyJpZCI6NjAxLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 601, - "uuid": "50d2f458-0565-41c7-94d3-c011f7528c9e", - "name": "platform:transaction-updated", - "sent": "2023-05-28 15:08:16", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 99, - "state": "BROADCAST", - "method": "BatchSetAttribute", - "result": null, - "transactionId": null, - "idempotencyKey": "0134b2af-b92d-4999-8900-3f8124172247", - "transactionHash": "0xd4e253623bc8170eef9cbe4537c08e063bd421ebb18b9943b9db7b75c5a0c03e" - } - } - }, - { - "cursor": "eyJpZCI6NjAyLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 602, - "uuid": "10b155ce-e514-4db8-9702-d1df475903a9", - "name": "platform:transaction-updated", - "sent": "2023-05-28 15:08:16", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 98, - "state": "BROADCAST", - "method": "CreateCollection", - "result": null, - "transactionId": null, - "idempotencyKey": "575992c4-911a-4d59-8ccf-7958d46e605b", - "transactionHash": "0xafecedea403e28722fb454b9be9bca5c55a9d0d7a62c30d80f2e2e64f78c4436" - } - } - }, - { - "cursor": "eyJpZCI6NjAzLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 603, - "uuid": "a6f22ab2-fd62-45a0-9c24-99b5c814f6b1", - "name": "platform:token-burned", - "sent": "2023-05-28 15:08:19", - "channels": [ - "collection;101728", - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", - "token;70468841277235617716769448539773927607", - "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "platform" - ], - "data": { - "amount": "1", - "wallet": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "tokenId": "70468841277235617716769448539773927607", - "collectionId": "101728", - "idempotencyKey": "4259ee51-f38e-4e57-907d-3da671340917" - } - } - }, - { - "cursor": "eyJpZCI6NjA0LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 604, - "uuid": "c7703b9d-08cd-4350-b342-2c263975cde0", - "name": "platform:collection-created", - "sent": "2023-05-28 15:08:19", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", - "platform" - ], - "data": { - "owner": "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", - "collectionId": "101732", - "idempotencyKey": "7c909267-1ff3-45c8-9651-b87a1697042a" - } - } - }, - { - "cursor": "eyJpZCI6NjA1LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 605, - "uuid": "2b8ac300-d970-4a56-8db5-e979d0827f47", - "name": "platform:collection-created", - "sent": "2023-05-28 15:08:19", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", - "platform" - ], - "data": { - "owner": "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", - "collectionId": "101733", - "idempotencyKey": "492af562-c4e9-418d-9707-5ce4a24680e1" - } - } - }, - { - "cursor": "eyJpZCI6NjA2LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 606, - "uuid": "cfeca74d-b0cf-42d9-bb5a-b8bf553b3127", - "name": "platform:transaction-updated", - "sent": "2023-05-28 15:08:19", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 97, - "state": "FINALIZED", - "method": "Burn", - "result": "EXTRINSIC_SUCCESS", - "transactionId": "865545-2", - "idempotencyKey": "4259ee51-f38e-4e57-907d-3da671340917", - "transactionHash": "0xd6f47b7c9ef3479b788ecaf76a203ebbefdb96f74fce3cac75ad84b0fc49f191" - } - } - }, - { - "cursor": "eyJpZCI6NjA3LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 607, - "uuid": "98a3449f-ccba-4f73-9a33-d4d9d55f979f", - "name": "platform:transaction-updated", - "sent": "2023-05-28 15:08:19", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 95, - "state": "FINALIZED", - "method": "CreateCollection", - "result": "EXTRINSIC_SUCCESS", - "transactionId": "865545-3", - "idempotencyKey": "7c909267-1ff3-45c8-9651-b87a1697042a", - "transactionHash": "0x6b1cdfaafcc5b343c02ce4593d56e0ad3bf2ffc814baac5e699a43e73c147a21" - } - } - } - ] + "GetExtrinsic": { + "id": "402865-2", + "hash": "0xbafe459e8248b802f3aef98d2e4a695bbb238899edf40519b082366e3ff8b98f", + "pallet": "MultiTokens", + "method": "mint", + "success": true, + "nonce": 132, + "signer": { "address": "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y" }, + "block": { "number": 402865 } } } } @@ -1079,37 +262,32 @@ query GetPendingEvents{ +:::warning GetExtrinsic and GetExtrinsics are currently unavailable +Both queries are temporarily disabled and return `Internal server error`. Use the Subscan workaround described above until the platform fix lands. +::: -### Use Case: - -You can use the `GetPendingEvents` query to ensure that all events are accounted for and processed in your system, even in the presence of network issues or disruptions. This query is crucial for: - -- Reconciling transaction records. -- Updating user interfaces in real-time. -- Triggering downstream processes that depend on event data. +## GetExtrinsics -Developers and system administrators can use this query to monitor and handle event-driven systems effectively, maintaining their reliability and consistency. To acknowledge the outputted events (and therefore remove them from pending events), set the `acknowledgeEvents` parameter to `true` (default is false). -To filter outputted pending events on specific channels, use the `channelFilters` parameter +Returns a list of extrinsics by `hashes`. Same temporary outage as `GetExtrinsic`. + + ```graphql -query geFilteredtPendingEvents { - GetPendingEvents( - channelFilters:[ - {type:OR filter:"cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu"} - {type:OR filter:"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"} - ]){ - edges { - node { - id - uuid - data - channels - } - } +query GetExtrinsics { + GetExtrinsics( + network: ENJIN + chain: MATRIX + hashes: [ + "0xbafe459e8248b802f3aef98d2e4a695bbb238899edf40519b082366e3ff8b98f" + "0x12ab34cd56ef78ab90cd12ef34ab56cd78ef90ab12cd34ef56ab78cd90ef12ab" + ] + ) { + hash + pallet + method + success } } ``` - -This example will output pending events broadcasted in either `cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu` and `cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f` channels. -Using the `Type` argument, you, can construct advanced filters like `'channel_1' AND ('channel_2' OR 'channel_3')` -Note, The default type is "**AND**". + + From e29b20377fe553bcb6fdb550094bf411d2073f08 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 25 May 2026 18:47:32 +0300 Subject: [PATCH 38/67] Rewrite Marketplace queries reference for v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop GetBid / GetBids / GetSale / GetSales — none exist in v3. Drop the Relay edges/node/pageInfo on GetListing(s), drop the nested sales/bids/states/seller.account connections that don't exist any more, and update GetListings to its real v3 signature (scoped to collectionId, offset-paginated by limit + page, optional tokenIds filter). Show the actual v3 Listing shape (id / seller{address} / makeAsset / takeAsset / amount / price / minTakeValue / isActive / highestPrice) and note the temporary Listing.type 500 bug with the inference workaround. --- .../01-queries/06-marketplace-queries.md | 585 +++--------------- 1 file changed, 74 insertions(+), 511 deletions(-) diff --git a/docs/03-api-reference/01-queries/06-marketplace-queries.md b/docs/03-api-reference/01-queries/06-marketplace-queries.md index aa9870f..3558961 100644 --- a/docs/03-api-reference/01-queries/06-marketplace-queries.md +++ b/docs/03-api-reference/01-queries/06-marketplace-queries.md @@ -1,7 +1,7 @@ --- title: "Marketplace" slug: "marketplace" -description: "Query the Enjin API for marketplace data, including listing information, token trades, and transaction details." +description: "Query the Enjin API for marketplace listings." --- import Tabs from '@theme/Tabs'; @@ -16,211 +16,38 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere `https://platform.beta.enjin.io/graphql` ::: -This is a detailed reference guide that explains the most commonly used operations. - -## GetBid - -The `GetBid` query allows you to retrieve detailed information about a specific bid placed on a market listing. It's a valuable tool for querying bid-related data, including bid price, bidder details, and the associated market listing. - - -```graphql -query GetBid { - GetBid(id: 1) { - id - price - height - bidder { - account { - publicKey - address - } - } - listing { - listingId - } - } -} -``` - - -```json -{ - "data": { - "GetBid": { - "id": 1, - "price": "1000000000000000000", - "height": 239058, - "bidder": { - "account": { - "publicKey": "0xb4664455021025f4944c1bc4af8a2830317f0765dc624778345dea09e89a526a", - "address": "cxNW84hdDPEr8rU8oUbqNDaCmVX6SxH86mFufTf6HiNbZhi2F" - } - }, - "listing": { - "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20" - } - } - } -} -``` - - - -## GetBids - -The `GetBids` query is used to retrieve a list of bids for one or more market listings based on specified listing IDs. Unlike the `GetBid` query, which retrieves details of a single bid, `GetBids` can return multiple bids and includes pagination information. - - - -```graphql -query GetBids { - GetBids(listingIds: ["0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20"]) { - totalCount - pageInfo { - startCursor - endCursor - hasPreviousPage - hasNextPage - } - edges { - node { - id - price - height - bidder { - account { - publicKey - address - } - } - listing { - listingId - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetBids": { - "totalCount": 1, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasPreviousPage": false, - "hasNextPage": false - }, - "edges": [ - { - "node": { - "id": 1, - "price": "1000000000000000000", - "height": 239058, - "bidder": { - "account": { - "publicKey": "0xb4664455021025f4944c1bc4af8a2830317f0765dc624778345dea09e89a526a", - "address": "cxNW84hdDPEr8rU8oUbqNDaCmVX6SxH86mFufTf6HiNbZhi2F" - } - }, - "listing": { - "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20" - } - } - } - ] - } - } -} -``` - - +The Enjin Platform marketplace exposes two read queries: `GetListing` for a single listing by id, and `GetListings` for a paginated list scoped to a collection. To act on a listing (place a bid, fill, cancel, finalize an auction, etc.) use [Marketplace Mutations](/03-api-reference/02-mutations/06-marketplace-mutations.md). ## GetListing -The `GetListing` query allows you to retrieve detailed information about a single market listing using its unique listing ID. This query provides a comprehensive overview of the listing's attributes, its state, sales history, bid history, and more. +Returns a single listing by its `id` (the hex listing id returned when the listing was created). ```graphql query GetListing { - GetListing(listingId: "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20") { - listingId - makeAssetId { - collectionId + GetListing( + network: ENJIN + chain: MATRIX + id: "8bd40957579f61b8a04ddaf7c86fbd51329e88f5ec735e910a320169377db161" + ) { + id + seller { + address + } + makeAsset { + id tokenId } - takeAssetId { - collectionId + takeAsset { + id tokenId } amount price minTakeValue - feeSide - creationBlock - deposit - salt - state { - ... on FixedPriceState { - amountFilled - type - } - ... on AuctionState { - type - } - } - data { - ... on FixedPriceData { - type - } - ... on AuctionData { - type - startBlock - endBlock - } - } - seller { - account { - publicKey - address - } - } - sales { - edges { - node { - amount - price - bidder { - account { - publicKey - address - } - } - } - } - } - bids { - edges { - node { - price - bidder { - account { - publicKey - address - } - } - height - } - } - } - states { - state - height - } + isActive + highestPrice } } ``` @@ -230,60 +57,15 @@ query GetListing { { "data": { "GetListing": { - "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20", - "makeAssetId": { - "collectionId": "4919", - "tokenId": "0" - }, - "takeAssetId": { - "collectionId": "0", - "tokenId": "0" - }, - "amount": "1", - "price": "1000000000000000000", - "minTakeValue": "975000000000000000", - "feeSide": "TAKE_FEE", - "creationBlock": 239058, - "deposit": "2025700000000000000", - "salt": "73616c74313233", - "state": { - "amountFilled": null, - "type": "FIXED_PRICE" - }, - "data": { - "type": "FIXED_PRICE" - }, - "seller": { - "account": { - "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", - "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" - } - }, - "sales": { - "edges": [] - }, - "bids": { - "edges": [ - { - "node": { - "price": "1000000000000000000", - "bidder": { - "account": { - "publicKey": "0xb4664455021025f4944c1bc4af8a2830317f0765dc624778345dea09e89a526a", - "address": "cxNW84hdDPEr8rU8oUbqNDaCmVX6SxH86mFufTf6HiNbZhi2F" - } - }, - "height": 239058 - } - } - ] - }, - "states": [ - { - "state": "ACTIVE", - "height": 239058 - } - ] + "id": "8bd40957579f61b8a04ddaf7c86fbd51329e88f5ec735e910a320169377db161", + "seller": { "address": "efRCJAEyn5Rp7SX9gvoJEAG7y1td3PZzRhpJYEeofpGVj3ouc" }, + "makeAsset": { "id": "3484-2", "tokenId": "2" }, + "takeAsset": null, + "amount": "72", + "price": "12000000000000000000", + "minTakeValue": "807840000000000000000", + "isActive": true, + "highestPrice": "12000000000000000000" } } } @@ -291,205 +73,37 @@ query GetListing { +Field notes: + +- `makeAsset` is the token being sold; `takeAsset` is the asset that fills the listing. When `takeAsset` is `null`, the listing is denominated in ENJ. +- `highestPrice` is `null` for fixed-price listings; for auctions it's the current top bid. +- `Listing.type` is part of the schema but currently returns an internal server error — omit it for now. The platform team is aware and a fix is planned. In the meantime, you can usually infer fixed-price vs. auction vs. offer from `highestPrice`/`takeAsset` shape. + ## GetListings -The `GetListings` query is used to retrieve information about multiple market listings from the Enjin Blockchain marketplace. Unlike the `GetListing` query, which fetches data for a single listing, `GetListings` is designed to handle multiple listings at once, making it suitable for scenarios where you need to fetch details for multiple listings in a single request. +Returns a paginated list of listings scoped to a single `collectionId`. Pagination is offset-based (`limit` + `page`). Optionally filter to specific `tokenIds`. ```graphql query GetListings { GetListings( - listingIds: ["0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20"] + network: ENJIN + chain: MATRIX + collectionId: 3484 + limit: 15 + page: 1 ) { - totalCount - pageInfo { - startCursor - endCursor - hasPreviousPage - hasNextPage - } - edges { - node { - listingId - makeAssetId { - collectionId - tokenId - } - takeAssetId { - collectionId - tokenId - } - amount - price - minTakeValue - feeSide - creationBlock - deposit - salt - state { - ... on FixedPriceState { - amountFilled - type - } - ... on AuctionState { - type - } - } - data { - ... on FixedPriceData { - type - } - ... on AuctionData { - type - startBlock - endBlock - } - } - seller { - account { - publicKey - address - } - } - sales { - edges { - node { - amount - price - bidder { - account { - publicKey - address - } - } - } - } - } - bids { - edges { - node { - price - bidder { - account { - publicKey - address - } - } - height - } - } - } - states { - state - height - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetListings": { - "totalCount": 1, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasPreviousPage": false, - "hasNextPage": false - }, - "edges": [ - { - "node": { - "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20", - "makeAssetId": { - "collectionId": "4919", - "tokenId": "0" - }, - "takeAssetId": { - "collectionId": "0", - "tokenId": "0" - }, - "amount": "1", - "price": "1000000000000000000", - "minTakeValue": "975000000000000000", - "feeSide": "TAKE_FEE", - "creationBlock": 239058, - "deposit": "2025700000000000000", - "salt": "73616c74313233", - "state": { - "amountFilled": null, - "type": "FIXED_PRICE" - }, - "data": { - "type": "FIXED_PRICE" - }, - "seller": { - "account": { - "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", - "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" - } - }, - "sales": { - "edges": [] - }, - "bids": { - "edges": [ - { - "node": { - "price": "1000000000000000000", - "bidder": { - "account": { - "publicKey": "0xb4664455021025f4944c1bc4af8a2830317f0765dc624778345dea09e89a526a", - "address": "cxNW84hdDPEr8rU8oUbqNDaCmVX6SxH86mFufTf6HiNbZhi2F" - } - }, - "height": 239058 - } - } - ] - }, - "states": [ - { - "state": "ACTIVE", - "height": 239058 - } - ] - } - } - ] - } - } -} -``` - - - -## GetSale - -The `GetSale` query is used to retrieve details about a specific sale transaction that has occurred on the Enjin Blockchain marketplace. This query requires the sale ID as input and returns information about the sale, including the sale ID, price, quantity, bidder information, and the associated market listing. - - - -```graphql -query GetSale { - GetSale(id: 1) { id - price - amount - bidder { - account { - publicKey - address - } + seller { + address } - listing { - listingId + makeAsset { + tokenId } + amount + price + isActive } } ``` @@ -498,95 +112,44 @@ query GetSale { ```json { "data": { - "GetSale": { - "id": 1, - "price": "1000000", - "amount": "1", - "bidder": { - "account": { - "publicKey": "0x6a03b1a3d40d7e344dfb27157931b14b59fe2ff11d7352353321fe400e956802", - "address": "cxLpbEojWougf2cE6onB6PuK9SsfmKqNJKNo9tTsqirxFbuN5" - } + "GetListings": [ + { + "id": "8bd40957579f61b8a04ddaf7c86fbd51329e88f5ec735e910a320169377db161", + "seller": { "address": "efRCJAEyn5Rp7SX9gvoJEAG7y1td3PZzRhpJYEeofpGVj3ouc" }, + "makeAsset": { "tokenId": "2" }, + "amount": "72", + "price": "12000000000000000000", + "isActive": true }, - "listing": { - "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20" + { + "id": "f3e76d1413e30548204dca8d0a9559dc981ce60e70b49fca4d031f7b7842a3c4", + "seller": { "address": "efUfmhm2rcTtVr4ajneB71qbfDZ1EkyYW7xRc6fhuRh3Tg4L1" }, + "makeAsset": { "tokenId": "4" }, + "amount": "3", + "price": "55000000000000000000", + "isActive": true } - } + ] } } ``` -## GetSales +### Filtering to specific token ids -The `GetSales` query is used to retrieve information about completed sale transactions that have occurred on a marketplace. This query allows you to fetch details of multiple sales associated with specific market listings. - - - ```graphql -query GetSales { - GetSales(listingIds: ["0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20"]) { - totalCount - pageInfo { - startCursor - endCursor - hasPreviousPage - hasNextPage - } - edges { - node { - id - price - amount - bidder { - account { - publicKey - address - } - } - listing { - listingId - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetSales": { - "totalCount": 1, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasPreviousPage": false, - "hasNextPage": false - }, - "edges": [ - { - "node": { - "id": 1, - "price": "1000000", - "amount": "1", - "bidder": { - "account": { - "publicKey": "0x6a03b1a3d40d7e344dfb27157931b14b59fe2ff11d7352353321fe400e956802", - "address": "cxLpbEojWougf2cE6onB6PuK9SsfmKqNJKNo9tTsqirxFbuN5" - } - }, - "listing": { - "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20" - } - } - } - ] - } +query GetListings { + GetListings( + network: ENJIN + chain: MATRIX + collectionId: 3484 + tokenIds: [2, 4] + limit: 15 + page: 1 + ) { + id + price } } ``` - - \ No newline at end of file From 6ce8e768415116c50932524807966b85c7564693 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 25 May 2026 18:48:42 +0300 Subject: [PATCH 39/67] Rewrite Collections mutations reference for v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All collection-level actions are now CreateTransaction discriminators rather than top-level mutations. Drop ApproveCollection / UnapproveCollection / Burn (the v2 burn-from-collection variant) — none have a v3 equivalent. Rewrite the surviving actions (createCollection, mutateCollection, destroyCollection, freezeCollection/thawCollection, setCollectionAttribute, removeCollectionAttribute) around the discriminator pattern, with flat input shapes (no mintPolicy/marketPolicy wrappers). Add the v3-new actions: acceptCollectionTransfer and cancelCollectionTransfer (paired with mutateCollection.owner for the ownership-transfer flow), batchSetCollectionAttribute, and removeAllCollectionAttributes. Cross-link the freezeToken/thawToken side to the Tokens mutations page. --- .../02-mutations/02-collections-mutations.md | 534 +++++++----------- 1 file changed, 192 insertions(+), 342 deletions(-) diff --git a/docs/03-api-reference/02-mutations/02-collections-mutations.md b/docs/03-api-reference/02-mutations/02-collections-mutations.md index ca83a88..3ee1780 100644 --- a/docs/03-api-reference/02-mutations/02-collections-mutations.md +++ b/docs/03-api-reference/02-mutations/02-collections-mutations.md @@ -1,7 +1,7 @@ --- title: "Collections" slug: "collections" -description: "Perform collections mutations in the Enjin API to manage your blockchain collections, including creating, modifying, and updating assets." +description: "Create, mutate, freeze, transfer, and destroy on-chain collections via CreateTransaction." --- import Tabs from '@theme/Tabs'; @@ -16,31 +16,43 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere `https://platform.beta.enjin.io/graphql` ::: -This is a detailed reference guide that explains the most commonly used operations. +All collection-level actions are submitted through [`CreateTransaction`](/03-api-reference/02-mutations/01-transaction-mutations.md#createtransaction), with the action selected by the field set on the `transaction` input. Each section below shows the discriminator and its expected fields. -## CreateCollection +The response shape is always a `Transaction` — the examples below all return the standard `{ uuid, action, state }` selection. -The `CreateCollection` mutation is used to create a new on-chain collection. A collection serves as a grouping of on-chain assets, typically non-fungible tokens (NFTs), that share common properties or belong to the same set. +## createCollection + +Creates a new on-chain collection. Most fields are optional — the example below shows everything; a minimal call is just `transaction: { createCollection: {} }`. ```graphql mutation CreateCollection { - CreateCollection{ - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - idempotencyKey + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + createCollection: { + forceCollapsingSupply: false + maxTokenCount: 1000 + maxTokenSupply: 100 + royalties: [ + { address: "efRoyaltyBeneficiary", percentage: 5.0 } + ] + attributes: [ + { key: "name", value: "Multiverse Brotherhood" } + { key: "uri", value: "https://example.com/metadata/collection.json" } + ] + explicitRoyaltyCurrencies: [ + { collectionId: 0, tokenId: 0 } + ] + } } + ) { + uuid + action + state + } } ``` @@ -48,20 +60,10 @@ mutation CreateCollection { ```json { "data": { - "CreateCollection": { - "id": 14058, - "transactionId": null, - "transactionHash": null, - "method": "CreateCollection", - "state": "PENDING", - "encodedData": "0x2800000001000000", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "ef403ad0-7b6c-4020-8ef1-01b1dc6f4513" + "CreateTransaction": { + "uuid": "a90ded41-4262-40a2-95c0-98255b660bf1", + "action": "MultiTokens.create_collection", + "state": "PENDING" } } } @@ -69,32 +71,35 @@ mutation CreateCollection { -## ApproveCollection +## mutateCollection + +Updates a collection's mutable fields — owner, royalties, royalty-currency whitelist, and attributes. -The `ApproveCollection` mutation is used to authorize another account (referred to as the "operator") to transfer tokens from a specific collection account. This operation is common in scenarios involving Non-Fungible Tokens (NFTs) or tokenized assets, where token management may need to be delegated without transferring ownership of the tokens. +Setting `owner` initiates an ownership transfer: the collection enters a pending state until the new owner calls [`acceptCollectionTransfer`](#acceptcollectiontransfer) (or the current owner calls [`cancelCollectionTransfer`](#cancelcollectiontransfer) to abort). ```graphql -mutation ApproveCollection { - ApproveCollection( - collectionId: "7154" - operator: "0x965bcdbb46614cbd79869e2eb568825f6c038cbdf9085edb1b164607d3738fa6" - expiration: 445100 - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } +mutation MutateCollection { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + mutateCollection: { + collectionId: 10943 + royalties: [ + { address: "efNewBeneficiaryAddress", percentage: 7.5 } + ] + attributes: [ + { key: "description", value: "Updated description" } + ] + } } + ) { + uuid + action + state + } } ``` @@ -102,19 +107,10 @@ mutation ApproveCollection { ```json { "data": { - "ApproveCollection": { - "id": 13819, - "transactionId": null, - "transactionHash": null, - "method": "ApproveCollection", - "state": "PENDING", - "encodedData": "0x280fc96f965bcdbb46614cbd79869e2eb568825f6c038cbdf9085edb1b164607d3738fa601acca0600", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } + "CreateTransaction": { + "uuid": "f02b8fef-8c0e-4f3f-9430-1cf6c2c20a23", + "action": "MultiTokens.mutate_collection", + "state": "PENDING" } } } @@ -122,373 +118,227 @@ mutation ApproveCollection { -## UnapproveCollection +## acceptCollectionTransfer -The `UnapproveCollection` mutation is used to revoke previously granted permissions for a specific account (referred to as the "operator") to transfer items from a collection owned by the sender's account. This operation is essential for enhancing the security and control of digital assets, ensuring that the owner of a collection can manage who has the authority to move or transfer items from their collection. +Called by the recipient of a pending collection transfer (set up via `mutateCollection.owner`) to complete the transfer. ```graphql -mutation UnapproveCollection{ - UnapproveCollection( - collectionId: "6305" - operator: "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "UnapproveCollection": { - "id": 14078, - "transactionId": null, - "transactionHash": null, - "method": "UnapproveCollection", - "state": "PENDING", - "encodedData": "0x28108562d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } +mutation AcceptCollectionTransfer { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + acceptCollectionTransfer: { id: 10943 } } + ) { + uuid + action + state } } ``` -## MutateCollection +## cancelCollectionTransfer -The `MutateCollection` mutation is utilized to modify the default settings of an existing collection. This mutation allows you to make changes to various parameters of a collection, such as adjusting royalty settings. It is particularly valuable when you need to alter how royalties are distributed for the assets within a specific collection. +Called by the current owner before a pending collection transfer is accepted, to abort it. ```graphql -mutation MutateCollection { - MutateCollection( - collectionId: "10943" - mutation: { - royalty: { - beneficiary:"0x50a1ba0a184c9aca3a2ac7d427e96a676fe988454b4b56a62dd6622e843e890d" - percentage: 50 - } - } - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "MutateCollection": { - "id": 14066, - "transactionId": null, - "transactionHash": null, - "method": "MutateCollection", - "state": "PENDING", - "encodedData": "0x2802fdaa00010150a1ba0a184c9aca3a2ac7d427e96a676fe988454b4b56a62dd6622e843e890d0294357700", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } +mutation CancelCollectionTransfer { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + cancelCollectionTransfer: { id: 10943 } } + ) { + uuid + action + state } } ``` -## Burn +## destroyCollection -The `Burn` mutation is used to permanently delete a specified amount of tokens from a collection. This operation is irreversible and results in the removal of tokens from circulation. When tokens are burned, the reserved value associated with them is often returned to the issuer's account. +Permanently destroys a collection. The collection must be empty (no tokens) and the deposit is returned to the owner. ```graphql -mutation Burn { - Burn( - collectionId: "7154" - params: { - tokenId: {integer:6533} - amount:1 - keepAlive:false - } - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "Burn": { - "id": 14055, - "transactionId": null, - "transactionHash": null, - "method": "Burn", - "state": "PENDING", - "encodedData": "0x2805c96f1566040000", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } +mutation DestroyCollection { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + destroyCollection: { id: 10942 } } + ) { + uuid + action + state } } ``` -## DestroyCollection +## freezeCollection / thawCollection -The `DestroyCollection` mutation is used to permanently delete an existing collection. This operation is irreversible and removes the entire collection, including all associated tokens and their reserved values. +Freeze or thaw an entire collection — while frozen, no tokens in the collection can be transferred, listed, or otherwise mutated. - + ```graphql -mutation DestroyCollection{ - DestroyCollection(collectionId: 10942) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } +mutation FreezeCollection { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + freezeCollection: { collectionId: 10943 } } + ) { + uuid + action + state + } } ``` - -```json -{ - "data": { - "DestroyCollection": { - "id": 14062, - "transactionId": null, - "transactionHash": null, - "method": "DestroyCollection", - "state": "PENDING", - "encodedData": "0x2801f9aa", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } + +```graphql +mutation ThawCollection { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + thawCollection: { collectionId: 10943 } } + ) { + uuid + action + state } } ``` -## Freeze +To freeze or thaw an individual token within a collection, use `freezeToken` / `thawToken` — see [Tokens Mutations](/03-api-reference/02-mutations/03-tokens-mutations.md). -The `Freeze` mutation is used to temporarily halt token transfers on a collection, token, collection account, or token account. Freezing temporarily prevents token transfers from taking place, which also results in marketplace listings being unpurchaseable. +## setCollectionAttribute + +Set or update a single key/value attribute on a collection. ```graphql -mutation Freeze { - Freeze( - freezeType: COLLECTION - collectionId: "10943" - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "Freeze": { - "id": 14064, - "transactionId": null, - "transactionHash": null, - "method": "Freeze", - "state": "PENDING", - "encodedData": "0x2807fdaa00", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } +mutation SetCollectionAttribute { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + setCollectionAttribute: { + collectionId: 4741 + key: "test" + value: "Hello" } } + ) { + uuid + action + state } } ``` -## SetCollectionAttribute +## batchSetCollectionAttribute -The `SetCollectionAttribute` mutation allows you to assign a new attribute or update an existing attribute's value within a collection. +Set multiple attributes on a collection in a single transaction. The collection id field is named `id` here (not `collectionId`). ```graphql -mutation SetCollectionAttribute { - SetCollectionAttribute( - collectionId: "4741" - key: "test" - value: "Hello" - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "SetCollectionAttribute": { - "id": 14072, - "transactionId": null, - "transactionHash": null, - "method": "SetCollectionAttribute", - "state": "PENDING", - "encodedData": "0x2809154a0010746573741448656c6c6f", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } +mutation BatchSetCollectionAttribute { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + batchSetCollectionAttribute: { + id: 4741 + attributes: [ + { key: "name", value: "My Collection" } + { key: "description", value: "A great collection" } + { key: "uri", value: "https://example.com/metadata/collection.json" } + ] } } + ) { + uuid + action + state } } ``` -## RemoveCollectionAttribute +## removeCollectionAttribute -The `RemoveCollectionAttribute` mutation is designed to remove a specific attribute from an entire collection of tokens. Attributes typically represent metadata associated with the tokens, such as name, color, size, or any other descriptive information. +Remove a single attribute from a collection. ```graphql mutation RemoveCollectionAttribute { - RemoveCollectionAttribute( - collectionId: "4741" + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + removeCollectionAttribute: { + id: 4741 key: "name" - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } + } } + ) { + uuid + action + state + } } ``` - -```json -{ - "data": { - "RemoveCollectionAttribute": { - "id": 14070, - "transactionId": null, - "transactionHash": null, - "method": "RemoveCollectionAttribute", - "state": "PENDING", - "encodedData": "0x280a154a00106e616d65", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } + + +## removeAllCollectionAttributes + +Remove every attribute from a collection in one transaction. + + + +```graphql +mutation RemoveAllCollectionAttributes { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + removeAllCollectionAttributes: { id: 4741 } } + ) { + uuid + action + state } } ``` - \ No newline at end of file + From dac569eee077e237719c0153c2b37564d6eec677 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 25 May 2026 18:51:35 +0300 Subject: [PATCH 40/67] Rewrite Tokens mutations reference for v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert every action to the CreateTransaction discriminator pattern. Drop ApproveToken / UnapproveToken / OperatorTransferToken / TransferAllBalance / TransferBalance — none have v3 equivalents. Rename Burn → burnToken (with the new removeTokenStorage flag), SimpleTransferToken → transferToken, BatchMint → mintTokens, Infuse → infuseToken / infuseTokens, Freeze → freezeToken / thawToken (the old freezeType discriminator is gone). Replace the v2 keepAlive flag with the v3 transferEnj action (no keepAlive field in v3). Add the v3-new actions: createTokens (batch), batchTransfer (uses 'address' on each recipient entry, not 'account'), the infuseTokens batch form, mutateToken's listingForbidden / anyoneCanInfuse fields, batchSetTokenAttribute, and removeAllTokenAttributes. Drop the {integer:N} tokenId wrapper — tokenId is a flat BigInt everywhere now. --- .../02-mutations/03-tokens-mutations.md | 1102 +++++------------ 1 file changed, 310 insertions(+), 792 deletions(-) diff --git a/docs/03-api-reference/02-mutations/03-tokens-mutations.md b/docs/03-api-reference/02-mutations/03-tokens-mutations.md index 73bd2f6..1c7ee4c 100644 --- a/docs/03-api-reference/02-mutations/03-tokens-mutations.md +++ b/docs/03-api-reference/02-mutations/03-tokens-mutations.md @@ -1,10 +1,9 @@ --- title: "Tokens" slug: "tokens" -description: "Use the Enjin API to perform tokens mutations, including minting new tokens, burning existing ones, and modifying token metadata." +description: "Create, mint, transfer, burn, freeze, infuse, and attribute tokens via CreateTransaction." --- -import GlossaryTerm from '@site/src/components/GlossaryTerm'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; @@ -17,41 +16,49 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere `https://platform.beta.enjin.io/graphql` ::: -This is a detailed reference guide that explains the most commonly used operations. +All token-level actions are submitted through [`CreateTransaction`](/03-api-reference/02-mutations/01-transaction-mutations.md#createtransaction), with the action selected by the field set on the `transaction` input. The response shape is always a `Transaction` — the examples below all return the standard `{ uuid, action, state }` selection. -## CreateToken +For collection-level actions (creating, mutating, freezing the whole collection, attributes), see [Collections Mutations](/03-api-reference/02-mutations/02-collections-mutations.md). -The `CreateToken` mutation enables you to create a new token within an existing collection. This operation is essential for introducing new digital assets, and it allows you to define various attributes and characteristics for the newly created token. +## createToken + +Creates a single new token in a collection, minting `initialSupply` units to `recipient`. Leave `cap` unset for an unlimited supply. ```graphql mutation CreateToken { - CreateToken( - recipient: "0xc4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3" - collectionId: "10941" - params: { - tokenId:{integer:1} - initialSupply:10 - cap: { - type:INFINITE - } - metadata: {name: "Gold Coins", symbol: "GOLD", decimalCount: 2} - } - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + createToken: { + recipient: "efRecipientAddress" + collectionId: 12345 + tokenId: 1 + initialSupply: 100 + listingForbidden: false + infusion: 0 + anyoneCanInfuse: false + cap: { type: SUPPLY, supply: 1000 } + behavior: { + type: HAS_ROYALTY + royalties: [ + { address: "efRoyaltyBeneficiary", percentage: 5.0 } + ] } + attributes: [ + { key: "name", value: "Legendary Sword" } + { key: "uri", value: "https://example.com/metadata/sword.json" } + ] + groups: [60] + } } + ) { + uuid + action + state + } } ``` @@ -59,19 +66,10 @@ mutation CreateToken { ```json { "data": { - "CreateToken": { - "id": 14060, - "transactionId": null, - "transactionHash": null, - "method": "CreateToken", - "state": "PENDING", - "encodedData": "0x280400c4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3f5aa00042800010000c16ff28623000000000000000000000000000000", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } + "CreateTransaction": { + "uuid": "a90ded41-4262-40a2-95c0-98255b660bf1", + "action": "MultiTokens.create_token", + "state": "PENDING" } } } @@ -79,952 +77,472 @@ mutation CreateToken { -:::info -For Token ID management, head to [Advamced Mechanics → TokenID Structure](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md) -::: +## createTokens -## MintToken - -The `MintToken` mutation allows you to increase the supply of an existing token. This operation is essential for managing the circulating supply of tokens and is typically used when you need to create additional units of a specific token. +Creates multiple tokens in the same collection in a single transaction. Each entry has the same fields as `createToken` minus the `collectionId` (which is set once at the top level). ```graphql -mutation MintToken { - MintToken( - recipient: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" - collectionId: "7154" - params: { - amount:1 - tokenId: {integer: 6533} - } - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "MintToken": { - "id": 14065, - "transactionId": null, - "transactionHash": null, - "method": "MintToken", - "state": "PENDING", - "encodedData": "0x280400aa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921c96f01156604010000c16ff28623000000000000000000", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } +mutation CreateTokens { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + createTokens: { + collectionId: 12345 + tokens: [ + { + recipient: "efRecipientAddress" + tokenId: 1 + initialSupply: 1 + listingForbidden: false + infusion: 0 + anyoneCanInfuse: false + } + { + recipient: "efRecipientAddress" + tokenId: 2 + initialSupply: 1 + listingForbidden: false + infusion: 0 + anyoneCanInfuse: false + } + ] } } + ) { + uuid + action + state } } ``` -## BatchMint +## mintToken -The `BatchMint` mutation enables you to efficiently create multiple tokens within a single blockchain transaction. This process, known as batch minting, simplifies the minting of multiple tokens, reducing transaction fees and processing time. The maximum number of recipients that you can batch into a single transaction is 250. +Mints additional units of an existing token. ```graphql -mutation BatchMint { - BatchMint( - collectionId: "7154" - recipients: [ - { - account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" - mintParams: { - amount:1 - tokenId: {integer: 6533} - } - }, - { - account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7215a17f847" - mintParams: { - amount:5 - tokenId: {integer: 4422} - } - } - ] - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "BatchMint": { - "id": 13928, - "transactionId": null, - "transactionHash": null, - "method": "BatchMint", - "state": "PENDING", - "encodedData": "0x280dc96f04aa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c292101156604010000c16ff28623000000000000000000", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } +mutation MintToken { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + mintToken: { + recipient: "efRecipientAddress" + collectionId: 12345 + tokenId: 1 + amount: 10 } } + ) { + uuid + action + state } } ``` -## SetTokenAttribute +## mintTokens -The `SetTokenAttribute` mutation allows you to assign or update attributes for a specific token within a collection. It is used to manage individual token metadata and properties. +The batched form of `mintToken`. All entries must belong to the same `collectionId`. ```graphql -mutation SetTokenAttribute{ - SetTokenAttribute( - collectionId: "4741" - tokenId: {integer:0} - key: "test" - value: "Hello" - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "SetTokenAttribute": { - "id": 14073, - "transactionId": null, - "transactionHash": null, - "method": "SetTokenAttribute", - "state": "PENDING", - "encodedData": "0x2809154a010000000000000000000000000000000010746573741448656c6c6f", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } +mutation MintTokens { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + mintTokens: { + collectionId: 12345 + tokens: [ + { recipient: "efAlice", tokenId: 1, amount: 5 } + { recipient: "efBob", tokenId: 2, amount: 10 } + ] } } + ) { + uuid + action + state } } ``` -## BatchSetAttribute +## transferToken -The `BatchSetAttribute` mutation allows you to efficiently set or update multiple attributes for a specific token within a collection in a single blockchain transaction. Attributes represent various properties, characteristics, or metadata associated with a token. +Transfers a token from the signer to a recipient. The signer (set via `signerAddress` / `signerExternalId` on `CreateTransaction`, or the Wallet Daemon by default) must hold the tokens being transferred. ```graphql -mutation BatchSetAttribute { - BatchSetAttribute( - collectionId: "7154" - tokenId: {integer:6533} - attributes: [ - { - key:"Name" - value:"John" - }, - { - key:"Surname" - value:"Snow" - }, - { - key:"House" - value:"Stark" - } - ] - ) { - id - transactionId - transactionHash - method - encodedData - state - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "BatchSetAttribute": { - "id": 13961, - "transactionId": null, - "transactionHash": null, - "method": "BatchSetAttribute", - "encodedData": "0x280ec96f01851900000000000000000000000000000410746573741468656c6c6f", - "state": "PENDING", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } +mutation TransferToken { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + transferToken: { + recipient: "efRecipientAddress" + collectionId: 12345 + tokenId: 1 + amount: 1 } } + ) { + uuid + action + state } } ``` -## BatchTransfer +## batchTransfer -The `BatchTransfer` mutation streamlines the process of transferring multiple tokens to one or more recipients within a single blockchain transaction. This is particularly useful when you need to distribute tokens to various accounts, such as rewarding multiple users or dispersing assets to different stakeholders. +Transfers multiple tokens (potentially to multiple recipients) from a single collection in one transaction. Each entry uses the field name `address` for the recipient — not `recipient`. ```graphql mutation BatchTransfer { - BatchTransfer( - collectionId: 36105 + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + batchTransfer: { + collectionId: 12345 recipients: [ - { - account:"cxM1YqYERiNvuEKkZUiUFefCtTfdy2q4Bj7QuWDfSPJuiuDWy" - simpleParams: { - tokenId: {integer:1} - amount:1 - } - }, - { - account:"cxKLTnnbsjsWQen47Sb73WfvCSsWAQfjNd6hRW86EqKC9Frhu" - simpleParams: { - tokenId: {integer:1} - amount:2 - } - }, - { - account:"cxM1YqYERiNvuEKkZUiUFefCtTfdy2q4Bj7QuWDfSPJuiuDWy" - simpleParams: { - tokenId: {integer:4} - amount:1 - } - } + { address: "efAlice", tokenId: 1, amount: 5 } + { address: "efBob", tokenId: 2, amount: 3 } ] - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "BatchTransfer": { - "id": 13974, - "transactionId": null, - "transactionHash": null, - "method": "BatchTransfer", - "state": "PENDING", - "encodedData": "0x280cc96f041013860712045df3393a7902eba3621115fdb7a6a4837349f7bc2fb676d57c000015660400", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } } } + ) { + uuid + action + state } } ``` -## SimpleTransferToken +## transferEnj -The `SimpleTransferToken` mutation simplifies the process of transferring a specific token from one wallet to another. It is a straightforward way to facilitate token transfers without the need for complex intermediary steps. +Transfers ENJ from the signer to a recipient. There is no `keepAlive` flag in v3. ```graphql -mutation SimpleTransferToken { - SimpleTransferToken( - collectionId: "7154" - recipient: "0xc4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3" - params: { - tokenId: {integer:70468841277235617716769448539773927607} - amount:1 - } - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "SimpleTransferToken": { - "id": 14074, - "transactionId": null, - "transactionHash": null, - "method": "SimpleTransferToken", - "state": "PENDING", - "encodedData": "0x280600c4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3c96f0033b7007b94fad4fa2fe1d6a5b71ece03350400", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } +mutation TransferEnj { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + transferEnj: { + recipient: "efRecipientAddress" + amount: 1000000000000000000 # 1 ENJ = 10^18 base units } } - } -} -``` - - - -## ApproveToken - -The `ApproveToken` mutation is a powerful tool for establishing fine-grained permission systems at the individual token level within a specific token collection. It enables a token owner to grant another account (the operator) the authority to transfer a defined number of tokens from their ownership to other accounts. This level of control is particularly valuable when managing tokens with varying values, attributes, or use cases within a collection. - - - -```graphql -mutation ApproveToken { - ApproveToken( - collectionId: "7154" - tokenId: {integer:6533} - operator: "0x965bcdbb46614cbd79869e2eb568825f6c038cbdf9085edb1b164607d3738fa6" - amount: 1 - currentAmount: 1 - expiration: 445300 ) { - id - transactionId - transactionHash - method + uuid + action state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "ApproveToken": { - "id": 13855, - "transactionId": null, - "transactionHash": null, - "method": "ApproveToken", - "state": "PENDING", - "encodedData": "0x2811c96f1566965bcdbb46614cbd79869e2eb568825f6c038cbdf9085edb1b164607d3738fa6040174cb060004", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } } } ``` -## UnapproveToken +For batched ENJ transfers (or for mixing ENJ transfers with other actions in a single extrinsic) use [`CreateBatchTransaction`](/03-api-reference/02-mutations/01-transaction-mutations.md#createbatchtransaction). + +## burnToken -The `UnapproveToken` mutation serves the purpose of revoking approval for a specific account (referred to as the "operator") to transfer a particular token from a token account. Unlike `UnapproveCollection`, which revokes permissions over an entire collection, `UnapproveToken` provides fine-grained control by targeting individual tokens within a collection. This level of precision is valuable when managing tokens with distinct values, attributes, or permissions within a collection. +Burns a quantity of a token, removing it from the holder's balance. Set `removeTokenStorage: true` if you don't intend to re-mint and want the per-token storage deposit returned (only applicable when the burn empties the token's supply). ```graphql -mutation UnapproveToken{ - UnapproveToken( - collectionId: 7154 - tokenId: {integer:70468841277235617716769448539773927607} - operator: "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "UnapproveToken": { - "id": 14079, - "transactionId": null, - "transactionHash": null, - "method": "UnapproveToken", - "state": "PENDING", - "encodedData": "0x2812c96f33b7007b94fad4fa2fe1d6a5b71ece0335d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } +mutation BurnToken { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + burnToken: { + collectionId: 12345 + tokenId: 1 + amount: 1 + removeTokenStorage: false } } - } -} -``` - - - -## Freeze - -The `Freeze` mutation is a powerful tool that enables the temporary suspension of token transfers on a collection, token, collection account, or token account. This freezing action effectively prevents any further transfers from occurring, which also results in marketplace listings being unpurchaseable. The primary purpose of freezing is to provide administrative control, enhance security, investigate suspicious activities, or respond to unforeseen events that require the temporary halt of operations. - - - -```graphql -mutation Freeze { - Freeze( - freezeType: COLLECTION - collectionId: "10943" ) { - id - transactionId - transactionHash - method + uuid + action state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "Freeze": { - "id": 14064, - "transactionId": null, - "transactionHash": null, - "method": "Freeze", - "state": "PENDING", - "encodedData": "0x2807fdaa00", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } } } ``` -## Thaw +## mutateToken -The `Thaw` mutation is a vital operation that allows you to unfreeze a previously frozen collection or token. Freezing temporarily prevents any transfers from occurring. Thawing reverses this action, restoring the collection or token's ability to be traded and transferred. +Updates a token's mutable fields — behavior, listing flag, infusion permission, on-chain name, attributes. ```graphql -mutation Thaw{ - Thaw( - freezeType: TOKEN - collectionId: 7154 - tokenId: {integer:70468841277235617716769448539773927607} - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "Thaw": { - "id": 14075, - "transactionId": null, - "transactionHash": null, - "method": "Thaw", - "state": "PENDING", - "encodedData": "0x2808c96f01b7007b94fad4fa2fe1d6a5b71ece03350101", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } +mutation MutateToken { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + mutateToken: { + collectionId: 12345 + tokenId: 1 + listingForbidden: true + anyoneCanInfuse: true + attributes: [ + { key: "description", value: "Updated description" } + ] } } + ) { + uuid + action + state } } ``` -## MutateToken +## freezeToken / thawToken -The `MutateToken` mutation is a powerful tool for modifying specific properties of an individual token within a collection. It provides fine-grained control over token attributes, allowing you to make precise changes to a token's characteristics. In the provided example, the mutation is used to set the `listingForbidden` attribute to `true`, which effectively prohibits the token from being listed for sale or trade. +Freeze or thaw an individual token. The `state` argument controls the freeze duration — `NEVER`, `TEMPORARY`, or `PERMANENT` — see [Freezing / Thawing Tokens](/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md) for the semantics of each. - + ```graphql -mutation MutateToken { - MutateToken( - collectionId: "7154" - tokenId: {integer:6533} - mutation: { - listingForbidden:true +mutation FreezeToken { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + freezeToken: { + collectionId: 12345 + tokenId: 1 + state: TEMPORARY + } } ) { - id - transactionId - transactionHash - method + uuid + action state - encodedData - wallet { - account { - publicKey - address - } - } } } ``` - -```json -{ - "data": { - "MutateToken": { - "id": 14067, - "transactionId": null, - "transactionHash": null, - "method": "MutateToken", - "state": "PENDING", - "encodedData": "0x2803c96f15660100010100", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } + +```graphql +mutation ThawToken { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + thawToken: { + collectionId: 12345 + tokenId: 1 + state: TEMPORARY } } + ) { + uuid + action + state } } ``` -## OperatorTransferToken +To freeze/thaw an entire collection at once, use `freezeCollection` / `thawCollection` — see [Collections Mutations](/03-api-reference/02-mutations/02-collections-mutations.md#freezecollection--thawcollection). + +## infuseToken / infuseTokens -The `OperatorTransferToken` mutation is designed for operators to transfer tokens on behalf of the token owner. It enables the transfer of a specified amount of tokens from a source address to a recipient address, with the operation performed by an authorized operator. +Add ENJ as backing into a token. The single form targets one token; the batched form takes a list of `{ tokenId, amount }` entries inside a single `collectionId`. - + ```graphql -mutation OperatorTransferToken{ - OperatorTransferToken( - collectionId: "7154" - recipient: "0x50a1ba0a184c9aca3a2ac7d427e96a676fe988454b4b56a62dd6622e843e890d" - params: { - tokenId: {integer:6533} - amount:1 - source:"0x1013860712045df3393a7902eba3621115fdb7a6a4837349f7bc2fb676d57c00" - } - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "OperatorTransferToken": { - "id": 14068, - "transactionId": null, - "transactionHash": null, - "method": "OperatorTransferToken", - "state": "PENDING", - "encodedData": "0x28060050a1ba0a184c9aca3a2ac7d427e96a676fe988454b4b56a62dd6622e843e890dc96f0115661013860712045df3393a7902eba3621115fdb7a6a4837349f7bc2fb676d57c000400", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } +mutation InfuseToken { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + infuseToken: { + collectionId: 12345 + tokenId: 1 + amount: 500000000000000000 # 0.5 ENJ } } - } -} -``` - - - -## RemoveAllAttributes - -Remove all attributes from a collection and token. - - - -```graphql -mutation RemoveAllAttributes { - RemoveAllAttributes( - collectionId: "7154" - tokenId: {integer:6533} ) { - id - transactionId - transactionHash - method + uuid + action state - encodedData - wallet { - account { - publicKey - address - } - } } } ``` - -```json -{ - "data": { - "RemoveAllAttributes": { - "id": 14069, - "transactionId": null, - "transactionHash": null, - "method": "RemoveAllAttributes", - "state": "PENDING", - "encodedData": "0x280bc96f018519000000000000000000000000000001000000", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } + +```graphql +mutation InfuseTokens { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + infuseTokens: { + collectionId: 12345 + tokens: [ + { tokenId: 1, amount: 500000000000000000 } + { tokenId: 2, amount: 1000000000000000000 } + ] } } + ) { + uuid + action + state } } ``` -## RemoveTokenAttribute +The signer must hold ENJ; if `anyoneCanInfuse` is `false` on the token, only the collection owner can infuse. + +## setTokenAttribute -The `RemoveTokenAttribute` mutation provides the capability to remove a specific attribute from an individual token within a collection. This operation is distinct from the `RemoveCollectionAttribute` mutation, which removes an attribute from all tokens within a collection. It is a precise way to manage token metadata, allowing for the removal of attributes that are no longer needed or contain errors. +Set or update a single key/value attribute on a token. ```graphql -mutation RemoveTokenAttribute { - RemoveTokenAttribute( - collectionId: "4741" - tokenId: {integer:0} - key: "name" - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "RemoveTokenAttribute": { - "id": 14071, - "transactionId": null, - "transactionHash": null, - "method": "RemoveTokenAttribute", - "state": "PENDING", - "encodedData": "0x280a154a0100000000000000000000000000000000106e616d65", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } +mutation SetTokenAttribute { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + setTokenAttribute: { + collectionId: 12345 + tokenId: 1 + key: "name" + value: "Legendary Sword" } } + ) { + uuid + action + state } } ``` -## TransferBalance +## batchSetTokenAttribute -The `TransferBalance` mutation is an operation designed for initiating the transfer of a specified amount of funds or balance from one account to another. It plays a fundamental role in enabling financial transactions by allowing users to send precise amounts of cryptocurrency or digital assets securely and transparently. +Set multiple attributes on a token in one transaction. ```graphql -mutation TransferBalance { - TransferBalance( - recipient: "0xc4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3" - amount: 1 - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "TransferBalance": { - "id": 14077, - "transactionId": null, - "transactionHash": null, - "method": "TransferBalance", - "state": "PENDING", - "encodedData": "0x0a0300c4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a304", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } +mutation BatchSetTokenAttribute { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + batchSetTokenAttribute: { + collectionId: 12345 + tokenId: 1 + attributes: [ + { key: "name", value: "Legendary Sword" } + { key: "description", value: "Forged in the Brotherhood" } + { key: "uri", value: "https://example.com/metadata/sword.json" } + ] } } + ) { + uuid + action + state } } ``` -## TransferAllBalance +## removeTokenAttribute -The `TransferAllBalance` mutation is designed for the purpose of transferring the entire balance from one account to another. It simplifies the process of moving all available funds from a sender's account to a recipient's account. This mutation is particularly useful for scenarios where you need to empty an account, consolidate funds from multiple accounts into one, or transfer ownership of an account by moving all assets to the new owner. +Remove a single attribute from a token. ```graphql -mutation TransferAllBalance { - TransferAllBalance( - recipient: "0xc4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3" - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "TransferAllBalance": { - "id": 14076, - "transactionId": null, - "transactionHash": null, - "method": "TransferAllBalance", - "state": "PENDING", - "encodedData": "0x0a0400c4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a301", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } +mutation RemoveTokenAttribute { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + removeTokenAttribute: { + collectionId: 12345 + tokenId: 1 + key: "name" } } + ) { + uuid + action + state } } ``` -## Infuse +## removeAllTokenAttributes -The Infuse mutation allows you to embed Enjin Coin (ENJ) into an existing token. This operation is crucial for enhancing the intrinsic value of digital assets, ensuring that they carry a guaranteed ENJ backing. By infusing ENJ, you provide tangible value to the token, which can only be retrieved through the process of "ing" the token. - -:::warning Calculating the infusion amount -The Platform accepts infusion values in the **base unit** (integers), not decimal ENJ amounts. To calculate the correct input, **multiply your desired ENJ amount by 10^18** (1 quintillion). - - **Formula:** `Desired ENJ infusion` \* `1,000,000,000,000,000,000` - - **Example:** To infuse a token with **5 ENJ**, input `5000000000000000000`. -::: +Remove every attribute from a token in one transaction. ```graphql -mutation Infuse{ - Infuse( - collectionId: 3298 #Specify the collection ID - tokenId: {integer: 1} #Specify the token ID - amount: 5000000000000000000 #Specify the amount of ENJ to infuse - ){ - id - method - state - } -} -``` - - -```json -{ - "data": { - "Infuse": { - "id": 386279, - "method": "Infuse", - "state": "PENDING" +mutation RemoveAllTokenAttributes { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + removeAllTokenAttributes: { + collectionId: 12345 + tokenId: 1 + } } + ) { + uuid + action + state } } ``` From f5c4732c1e4c1b8ddef696d5595c0f11cc01d494 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 25 May 2026 18:52:52 +0300 Subject: [PATCH 41/67] Rewrite Marketplace mutations reference for v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert the 4 surviving v2 mutations (createListing, fillListing, finalizeAuction, cancelListing) to the CreateTransaction discriminator pattern. Flatten the v2 listingData (auctionParams / offerParams wrappers are gone — startBlock/endBlock/expiration are direct fields on ListingDataInput), drop the salt argument (no longer required), and drop the {integer:N} tokenId wrapper on makeAssetId/takeAssetId. Add the v3-new actions: placeBid (auctions), placeCounterOffer + answerCounterOffer (offer-listing negotiation), and addWhitelistedAccounts + removeWhitelistedAccounts (whitelist management for usesWhitelist listings). --- .../02-mutations/06-marketplace-mutations.md | 450 +++++++++--------- 1 file changed, 228 insertions(+), 222 deletions(-) diff --git a/docs/03-api-reference/02-mutations/06-marketplace-mutations.md b/docs/03-api-reference/02-mutations/06-marketplace-mutations.md index 75f4a4c..6a31277 100644 --- a/docs/03-api-reference/02-mutations/06-marketplace-mutations.md +++ b/docs/03-api-reference/02-mutations/06-marketplace-mutations.md @@ -1,7 +1,7 @@ --- title: "Marketplace" slug: "marketplace" -description: "Use the Enjin API to perform marketplace mutations, including creating and modifying token listings, managing trades, and marketplace transactions." +description: "Create, fill, cancel, finalize, bid, counter-offer, and whitelist marketplace listings via CreateTransaction." --- import Tabs from '@theme/Tabs'; @@ -16,104 +16,95 @@ For the most up-to-date information, refer to the [API Reference](/03-api-refere `https://platform.beta.enjin.io/graphql` ::: -This is a detailed reference guide that explains the most commonly used operations. +All marketplace actions are submitted through [`CreateTransaction`](/03-api-reference/02-mutations/01-transaction-mutations.md#createtransaction), with the action selected by the field set on the `transaction` input. The response shape is always a `Transaction` — the examples below all return the standard `{ uuid, action, state }` selection. -## CreateListing +To read listings, see [Marketplace Queries](/03-api-reference/01-queries/06-marketplace-queries.md). -The `CreateListing` mutation is used to initiate the process of placing a buy or a sell order on a marketplace. This mutation is essential for sellers who want to list their assets for sale in the marketplace, or buyers who wants to purchase a token. Buy orders are Offers, and sell orders are either Fixed Listings or Auction Listings. +## createListing -### Use Case: +Creates a new listing on the marketplace. Three listing types are supported via `listingData.type`: -The `CreateListing` mutation is a fundamental operation in a marketplace. It allows you, as a seller, to list your assets for sale on the platform, and as a buyer, to create a buy offer. Here's how you can use it: +- **`FIXED_PRICE`** — sell `amount` of `makeAssetId` for `price` of `takeAssetId`. +- **`AUCTION`** — same shape, but buyers place bids via [`placeBid`](#placebid); the auction ends at `endBlock`. +- **`OFFER`** — a *buy* offer: the seller of `takeAssetId` decides whether to fill. -1. **Asset Details**: Specify the asset you want to buy/sell and the asset you want to receive in exchange. - When creating a sell order, specify the asset you want to sell in the `makeAssetId` parameter, and the asset you want to receive in exchange in the `takeAssetId` parameter. - When creating a buy order, specify the asset you want to buy in the `takeAssetId` parameter, and the asset you want to pay with in the `makeAssetId` parameter. - -:::info -For ENJ tokens, use `collectionId: 0` and `tokenId: {integer: 0}` -::: - -2. **Quantity and Price**: Set the quantity (`amount`) and the price (`price`) at which you are willing to buy/sell the asset. -3. **Uniqueness**: Include a random string (`salt`) to ensure the uniqueness of your listing order. -4. **start/end block (Optional)**: If you are conducting an auction, you can specify the start and end blocks for the auction (`auctionParams`). - If you are conducting an offer, you can specify the end block for the auction (`offerParams`). -5. **Response**: After successfully creating the listing, you will receive a response with details about the new listing, including its ID, state, and other relevant information. - -This mutation is essential for both buyers and sellers to participate in the marketplace, list their assets and offers, and potentially initiate sales or auctions. It forms the basis for trading assets within the platform. - -### Create Fixed Listing +Set `takeAssetId` to ENJ by passing `{ collectionId: 0, tokenId: 0 }`. Set `usesWhitelist: true` to restrict who can fill / bid; manage the whitelist with [`addWhitelistedAccounts`](#addwhitelistedaccounts) / [`removeWhitelistedAccounts`](#removewhitelistedaccounts). - + ```graphql -mutation CreateOffer{ - CreateListing( - makeAssetId: { - collectionId: 91829 - tokenId: {integer: 1} - } - takeAssetId: { - collectionId: 0 - tokenId: {integer: 0} - } - amount: 1 - price: 10000000000000000000 - salt: "random string" - listingData: { - type: FIXED_PRICE +mutation CreateFixedListing { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + createListing: { + makeAssetId: { collectionId: 91829, tokenId: 1 } + takeAssetId: { collectionId: 0, tokenId: 0 } # ENJ + amount: 1 + price: 10000000000000000000 # 10 ENJ + usesWhitelist: false + listingData: { type: FIXED_PRICE } + } } - ){ - id - method + ) { + uuid + action state } } ``` - -```json -{ - "data": { - "CreateListing": { - "id": 386427, - "method": "CreateListing", - "state": "PENDING" + +```graphql +mutation CreateAuctionListing { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + createListing: { + makeAssetId: { collectionId: 91829, tokenId: 1 } + takeAssetId: { collectionId: 0, tokenId: 0 } + amount: 1 + price: 5000000000000000000 # 5 ENJ reserve + usesWhitelist: false + listingData: { + type: AUCTION + startBlock: 3818000 + endBlock: 3820000 + } + } } + ) { + uuid + action + state } } ``` - - -### Create Auction Listing - - - + ```graphql -mutation CreateOffer{ - CreateListing( - makeAssetId: { - collectionId: 91829 - tokenId: {integer: 1} - } - takeAssetId: { - collectionId: 0 - tokenId: {integer: 0} - } - amount: 1 - price: 5000000000000000000 - salt: "random string" - listingData: { - type: AUCTION - auctionParams: { - startBlock: 3818000 - endBlock: 1000000000 +mutation CreateOfferListing { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + createListing: { + makeAssetId: { collectionId: 0, tokenId: 0 } # paying with ENJ + takeAssetId: { collectionId: 91829, tokenId: 1 } # asking for this token + amount: 1 + price: 5000000000000000000 + usesWhitelist: false + listingData: { + type: OFFER + expiration: 100000000 + } } } - ){ - id - method + ) { + uuid + action state } } @@ -123,9 +114,9 @@ mutation CreateOffer{ ```json { "data": { - "CreateListing": { - "id": 386422, - "method": "CreateListing", + "CreateTransaction": { + "uuid": "a90ded41-4262-40a2-95c0-98255b660bf1", + "action": "Marketplace.create_listing", "state": "PENDING" } } @@ -134,215 +125,230 @@ mutation CreateOffer{ -### Create Offer Listing +The listing's id is the hex string returned by the chain once the transaction finalizes — read it from `Marketplace.ListingCreated` events on the extrinsic, or by listing the collection via [`GetListings`](/03-api-reference/01-queries/06-marketplace-queries.md#getlistings). + +## fillListing + +Buys (or sells, for an offer listing) `amount` of a `FIXED_PRICE` / `OFFER` listing. ```graphql -mutation CreateOffer{ - CreateListing( - makeAssetId: { - collectionId: 0 - tokenId: {integer: 0} - } - takeAssetId: { - collectionId: 91829 - tokenId: {integer: 1} - } - amount: 1 - price: 5000000000000000000 - salt: "random string" - listingData: { - type: OFFER - offerParams: { - expiration: 100000000 +mutation FillListing { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + fillListing: { + id: "8bd40957579f61b8a04ddaf7c86fbd51329e88f5ec735e910a320169377db161" + amount: 1 } } - ){ - id - method + ) { + uuid + action state } } ``` - -```json -{ - "data": { - "CreateListing": { - "id": 386394, - "method": "CreateListing", - "state": "PENDING" + + +## placeBid + +Place a bid on an `AUCTION` listing. The bid must be at least the current `highestPrice + minimum increment` (or the reserve `price` for the first bid). + + + +```graphql +mutation PlaceBid { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + placeBid: { + id: "8bd40957579f61b8a04ddaf7c86fbd51329e88f5ec735e910a320169377db161" + price: 12000000000000000000 # 12 ENJ + } } + ) { + uuid + action + state } } ``` -## FillListing +## finalizeAuction -The `FillListing` mutation is used to finalize the purchase of an asset listed at a fixed price on a marketplace, as well as offers. It enables buyers to accept the terms of a listing and execute the transaction to acquire the asset they desire and enables sellers to accept an offer. +Closes an auction past its `endBlock`, transferring the asset to the highest bidder and the bid to the seller. Anyone can call this. ```graphql -mutation FillListing { - FillListing(listingId: "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20", amount: 1) { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address +mutation FinalizeAuction { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + finalizeAuction: { + id: "8bd40957579f61b8a04ddaf7c86fbd51329e88f5ec735e910a320169377db161" } } - idempotencyKey + ) { + uuid + action + state } } ``` - -```json -{ - "data": { - "FillListing": { - "id": 11297, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x3202101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f2004", - "method": "FillListing", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "0f4eb316-cf7f-4561-a2d6-112abf08d323" + + +## cancelListing + +Cancels an active listing. The signer must be the listing's seller. + + + +```graphql +mutation CancelListing { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + cancelListing: { + id: "8bd40957579f61b8a04ddaf7c86fbd51329e88f5ec735e910a320169377db161" + } } + ) { + uuid + action + state } } ``` -### Use Case: - -The `FillListing` mutation is a critical operation for buyers in a marketplace. Here's how you can use it: +## placeCounterOffer -1. **Select Listing**: You specify the unique `listingId` of the listing you want to fill, indicating the asset you wish to purchase. -2. **Quantity**: Determine the `amount` of the asset you want to buy. Ensure that this quantity matches the available quantity in the listing if it's a fixed price sale. -3. **Transaction Submission**: Execute the mutation, triggering the transaction. The response will provide transaction details, including the initial state as "PENDING." -4. **Transaction Processing**: The network processes the transaction, and eventually, the `transactionId` and `transactionHash` fields in the response will be updated with the respective values once confirmed. -5. **Completion**: The successful execution of the `FillListing` mutation indicates that you have committed to the purchase, and the asset will be transferred to your wallet upon transaction confirmation. -6. **Accepting Offers**: The mutation is also used by seller to accept a buy offer and sell the asset to the buyer. - -This mutation is essential for buyers to participate in the marketplace, finalize purchases, and take ownership of assets. It facilitates the transfer of both digital assets and funds between parties on the blockchain, ensuring a seamless and secure transaction process. - -## FinalizeAuction - -The `FinalizeAuction` mutation is used to complete the auction process for a specific asset listed in a marketplace. This operation is crucial for officially closing the auction and transferring the asset to the highest bidder, typically after the auction has ended. +On an `OFFER` listing, place a counter-offer with a different `price`. The original listing creator can then [accept, reject, or counter again](#answercounteroffer). ```graphql -mutation FinalizeAuction { - FinalizeAuction(listingId: "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20") { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address +mutation PlaceCounterOffer { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + placeCounterOffer: { + id: "8bd40957579f61b8a04ddaf7c86fbd51329e88f5ec735e910a320169377db161" + price: 6000000000000000000 } } - idempotencyKey + ) { + uuid + action + state } } ``` - -```json -{ - "data": { - "FinalizeAuction": { - "id": 11298, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x3204101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20", - "method": "FinalizeAuction", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "0deaaad8-f6a0-4ef5-b8f1-fadd1ecb8ac5" + + +## answerCounterOffer + +Respond to a counter-offer placed via `placeCounterOffer`. `response` is one of: + +- `ACCEPT` — fill the listing at the counter-offered price. +- `REJECT` — decline the counter-offer. +- `COUNTER` — make a new counter-offer at `counterPrice`. + + + +```graphql +mutation AnswerCounterOffer { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + answerCounterOffer: { + id: "8bd40957579f61b8a04ddaf7c86fbd51329e88f5ec735e910a320169377db161" + creator: "efCounterOfferer" + response: COUNTER + counterPrice: 5500000000000000000 + } } + ) { + uuid + action + state } } ``` -## CancelListing +## addWhitelistedAccounts -The `CancelListing` mutation in used to cancel a listing on the Enjin Blockchain marketplace. This operation is typically performed by the seller or the system administrator and is used to remove an asset from a marketplace listing. +Add accounts (with optional per-account `allowance`) to a whitelist-restricted listing. ```graphql -mutation CancelListing { - CancelListing(listingId: "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20") { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address +mutation AddWhitelistedAccounts { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + addWhitelistedAccounts: { + id: "8bd40957579f61b8a04ddaf7c86fbd51329e88f5ec735e910a320169377db161" + accounts: [ + { address: "efAllowedBuyer1", allowance: 5 } + { address: "efAllowedBuyer2" } + ] } } - idempotencyKey + ) { + uuid + action + state } } ``` - -```json -{ - "data": { - "CancelListing": { - "id": 11295, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x3201101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20", - "method": "CancelListing", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "bfdb8fe9-3d93-4322-bf16-077c9566050d" + + +## removeWhitelistedAccounts + +Remove accounts from a whitelist-restricted listing. + + + +```graphql +mutation RemoveWhitelistedAccounts { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + removeWhitelistedAccounts: { + id: "8bd40957579f61b8a04ddaf7c86fbd51329e88f5ec735e910a320169377db161" + addresses: [ + "efRevokedBuyer1" + "efRevokedBuyer2" + ] + } } + ) { + uuid + action + state } } ``` - \ No newline at end of file + From b3f87b940289de4fd3769a377c44dff02c035924 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 25 May 2026 18:55:32 +0300 Subject: [PATCH 42/67] Add API reference for token groups and nomination pools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Token groups and Relaychain nomination pools are first-class v3 concepts with no analog in the v2 docs. Document each on its own queries + mutations page so they don't get buried inside the collections / accounts pages, and link them from the API reference index. Token-group queries cover the two access paths the schema actually exposes (Collection.tokenGroups and Token.tokenGroupTokens — there's no GetTokenGroup root query). Mutations cover the seven discriminators on TransactionInput (createTokenGroup, destroyTokenGroup, addTokenToGroup, removeTokenFromGroup, setTokenGroups, setTokenGroupAttribute, removeTokenGroupAttribute) plus the createToken.groups shortcut. Nomination-pool queries cover GetNominationPool, GetNominationPools, and GetAccountPools; mutations cover nominationPoolsBond (amount vs fill: true) and nominationPoolsUnbond. Both sides flag the Relaychain chain: RELAY requirement up top. --- .../01-queries/07-token-groups-queries.md | 155 ++++++++++++ .../01-queries/08-nomination-pool-queries.md | 188 ++++++++++++++ .../02-mutations/07-token-groups-mutations.md | 238 ++++++++++++++++++ .../08-nomination-pool-mutations.md | 112 +++++++++ docs/03-api-reference/03-api-reference.md | 4 +- 5 files changed, 695 insertions(+), 2 deletions(-) create mode 100644 docs/03-api-reference/01-queries/07-token-groups-queries.md create mode 100644 docs/03-api-reference/01-queries/08-nomination-pool-queries.md create mode 100644 docs/03-api-reference/02-mutations/07-token-groups-mutations.md create mode 100644 docs/03-api-reference/02-mutations/08-nomination-pool-mutations.md diff --git a/docs/03-api-reference/01-queries/07-token-groups-queries.md b/docs/03-api-reference/01-queries/07-token-groups-queries.md new file mode 100644 index 0000000..af9cea2 --- /dev/null +++ b/docs/03-api-reference/01-queries/07-token-groups-queries.md @@ -0,0 +1,155 @@ +--- +title: "Token Groups" +slug: "token-groups" +description: "Read token groups via Collection.tokenGroups and Token.tokenGroupTokens." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` +::: + +A **token group** is an on-chain set of tokens within a single collection. Each token can belong to zero or more groups; each group can hold many tokens. Groups make it easier to organise large collections in marketplaces and games — for example, you might create a "Legendary Swords" group inside a `Multiverse Brotherhood` collection and grant in-game utility to anyone holding a token from that group. + +There is no dedicated `GetTokenGroup` root query. Read groups via the two sides they're related to: + +- **`Collection.tokenGroups`** — every group defined inside a collection. +- **`Token.tokenGroupTokens`** — every group a particular token belongs to. + +To create, modify, or delete a group, see [Token Groups Mutations](/03-api-reference/02-mutations/07-token-groups-mutations.md). + +## Reading all groups in a collection + +Use [`GetCollection`](/03-api-reference/01-queries/02-collections-queries.md#getcollection) and select `tokenGroups`. Each group exposes its `id`, `attributes`, `metadata`, and the tokens in it via `tokenGroupTokens`. + + + +```graphql +query GetCollectionGroups { + GetCollection(network: ENJIN, chain: MATRIX, id: 3484) { + id + tokenGroups { + id + attributes { + key + value + } + metadata { + name + description + } + tokenGroupTokens { + tokenId + position + } + } + } +} +``` + + +```json +{ + "data": { + "GetCollection": { + "id": "3484", + "tokenGroups": [ + { + "id": "60", + "attributes": [ + { "key": "uri", "value": "https://example.com/metadata/token-groups/clan-tag.json" } + ], + "metadata": { + "name": "[MvB] Clan Tag", + "description": null + }, + "tokenGroupTokens": [ + { "tokenId": "3484-107335160659631965554526087350803497388", "position": 0 }, + { "tokenId": "3484-107335160659631965554526087350803497398", "position": 0 } + ] + } + ] + } + } +} +``` + + + +`tokenGroupTokens[].tokenId` is the canonical token id (`"-"`), not the raw `tokenId` integer. + +:::warning Selecting TokenGroup.collectionId returns an error +The `collectionId` field on `TokenGroup` is in the schema but currently returns an `Internal server error` when selected. Omit it — the parent `Collection.id` already gives you the collection id. The platform team is aware and a fix is planned. +::: + +## Reading the groups a token belongs to + +Use [`GetToken`](/03-api-reference/01-queries/03-tokens-queries.md#gettoken) and select `tokenGroupTokens` → `tokenGroup`. This is the lookup to use when you need to check whether a player's token grants in-game utility tied to a group. + + + +```graphql +query GetTokenGroups { + GetToken( + network: ENJIN + chain: MATRIX + id: "3484-107335160659631965554526087350803497388" + ) { + id + tokenId + tokenGroupTokens { + tokenGroupId + position + tokenGroup { + id + metadata { + name + } + } + } + } +} +``` + + +```json +{ + "data": { + "GetToken": { + "id": "3484-107335160659631965554526087350803497388", + "tokenId": "107335160659631965554526087350803497388", + "tokenGroupTokens": [ + { + "tokenGroupId": "60", + "position": 0, + "tokenGroup": { + "id": "60", + "metadata": { + "name": "[MvB] Clan Tag" + } + } + } + ] + } + } +} +``` + + + +## Checking whether a wallet holds any token from a specific group + +There's no single-query path for this — the v3 API doesn't expose a per-(account, group) join. The pattern is: + +1. `GetAccount(address:)` → read the wallet's `tokens` list. +2. For each held token (or for the tokens you care about), select `tokenGroupTokens { tokenGroupId }`. +3. Check whether the target group id appears. + +Alternatively, since the platform may evolve this surface, you can read [`Collection.tokenGroups[].tokenGroupTokens`](#reading-all-groups-in-a-collection) once and cache it, then intersect against the wallet's token ids. diff --git a/docs/03-api-reference/01-queries/08-nomination-pool-queries.md b/docs/03-api-reference/01-queries/08-nomination-pool-queries.md new file mode 100644 index 0000000..b964465 --- /dev/null +++ b/docs/03-api-reference/01-queries/08-nomination-pool-queries.md @@ -0,0 +1,188 @@ +--- +title: "Nomination Pools" +slug: "nomination-pools" +description: "Read Relaychain nomination pools and an account's pool memberships." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` +::: + +Nomination pools live on the **Relaychain** — pass `chain: RELAY` on every query in this section. They let many small ENJ holders pool their stake under a single nominator, sharing rewards proportionally. To bond into a pool or unbond, see [Nomination Pool Mutations](/03-api-reference/02-mutations/08-nomination-pool-mutations.md). + +## GetNominationPool + +Returns a single nomination pool by `id`. + + + +```graphql +query GetNominationPool { + GetNominationPool(network: ENJIN, chain: RELAY, id: 1) { + id + name + state + capacity + balance { + stash + reward + active + } + commission + apy + totalMembers + members { + publicKey + bonded + unrealisedEnj + } + } +} +``` + + +```json +{ + "data": { + "GetNominationPool": { + "id": 1, + "name": "", + "state": "OPEN", + "capacity": "32000000000000000000000000", + "balance": { + "stash": "17679671198897628191743870", + "reward": "100000000000000000", + "active": "37463637246328117435530629" + }, + "commission": 0, + "apy": 22.636730650749662, + "totalMembers": 195, + "members": [ + { + "publicKey": "0xded3c8a93e4a4b3a5e8f3c9a2d1b6f0e7c8a9b0d1e2f3a4b5c6d7e8f9a0b1c2d", + "bonded": "5000000000000000000000", + "unrealisedEnj": "0" + } + ] + } + } +} +``` + + + +Field notes: + +- `state` is one of `OPEN`, `BLOCKED`, `DESTROYING`, `DESTROYED`. You can only bond into `OPEN` pools. +- `capacity` is the maximum bondable amount, in the smallest ENJ unit. +- `balance.stash` / `.active` / `.reward` describe the pool's on-chain state — `active` is the currently-staked portion that earns rewards. +- `commission` is a fraction in the range `0..1` (e.g. `0.05` means the pool operator takes 5%). +- `apy` is a percentage (e.g. `22.6` means ~22.6% annualised). +- `members[].unrealisedEnj` is the member's pending reward not yet claimed. + +## GetNominationPools + +Returns a paginated list of pools. Pagination is offset-based (`limit` + `page`). + + + +```graphql +query GetNominationPools { + GetNominationPools(network: ENJIN, chain: RELAY, limit: 15, page: 1) { + id + name + state + commission + apy + totalMembers + balance { + active + } + } +} +``` + + +```json +{ + "data": { + "GetNominationPools": [ + { + "id": 1, + "name": "", + "state": "OPEN", + "commission": 0, + "apy": 22.636730650749662, + "totalMembers": 195, + "balance": { "active": "37463637246328117435530629" } + }, + { + "id": 2, + "name": "Pool Two", + "state": "OPEN", + "commission": 0.05, + "apy": 21.8, + "totalMembers": 87, + "balance": { "active": "12300000000000000000000000" } + } + ] + } +} +``` + + + +## GetAccountPools + +Returns every pool a given `address` is a member of, with that account's per-pool bonded amount and unrealised rewards. + + + +```graphql +query GetAccountPools { + GetAccountPools( + network: ENJIN + chain: RELAY + address: "efStakerAddress" + ) { + id + name + state + bonded + unrealisedEnj + totalUnbonding + apy + } +} +``` + + +```json +{ + "data": { + "GetAccountPools": [ + { + "id": 1, + "name": "", + "state": "OPEN", + "bonded": "5000000000000000000000", + "unrealisedEnj": "12500000000000000", + "totalUnbonding": "0", + "apy": 22.636730650749662 + } + ] + } +} +``` + + + +`totalUnbonding` is the amount the account has requested to unbond but hasn't yet withdrawn (Substrate nomination pools have an unbonding period before unbonded ENJ becomes spendable). diff --git a/docs/03-api-reference/02-mutations/07-token-groups-mutations.md b/docs/03-api-reference/02-mutations/07-token-groups-mutations.md new file mode 100644 index 0000000..25f865e --- /dev/null +++ b/docs/03-api-reference/02-mutations/07-token-groups-mutations.md @@ -0,0 +1,238 @@ +--- +title: "Token Groups" +slug: "token-groups" +description: "Create, destroy, add/remove tokens to, and attribute on-chain token groups via CreateTransaction." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` +::: + +A token group is an on-chain bucket of tokens within a single collection — see [Token Groups Queries](/03-api-reference/01-queries/07-token-groups-queries.md) for the read side. All token-group actions are submitted through [`CreateTransaction`](/03-api-reference/02-mutations/01-transaction-mutations.md#createtransaction), with the action selected by the field set on the `transaction` input. The response shape is always a `Transaction` — the examples below all return the standard `{ uuid, action, state }` selection. + +## createTokenGroup + +Creates an empty token group inside a collection. The group's `id` is assigned by the chain; read it back via [`GetCollection.tokenGroups`](/03-api-reference/01-queries/07-token-groups-queries.md#reading-all-groups-in-a-collection) once the transaction finalizes (or by inspecting the `MultiTokens.TokenGroupCreated` event on Subscan). + + + +```graphql +mutation CreateTokenGroup { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + createTokenGroup: { collectionId: 3484 } + } + ) { + uuid + action + state + } +} +``` + + +```json +{ + "data": { + "CreateTransaction": { + "uuid": "a90ded41-4262-40a2-95c0-98255b660bf1", + "action": "MultiTokens.create_token_group", + "state": "PENDING" + } + } +} +``` + + + +## destroyTokenGroup + +Destroys an existing token group. The group must be empty (no tokens) — remove tokens first with [`removeTokenFromGroup`](#removetokenfromgroup) or [`setTokenGroups`](#settokengroups). + + + +```graphql +mutation DestroyTokenGroup { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + destroyTokenGroup: { id: 60 } + } + ) { + uuid + action + state + } +} +``` + + + +## addTokenToGroup + +Adds a single token to a group. + + + +```graphql +mutation AddTokenToGroup { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + addTokenToGroup: { + collectionId: 3484 + tokenId: 107335160659631965554526087350803497388 + groupId: 60 + } + } + ) { + uuid + action + state + } +} +``` + + + +## removeTokenFromGroup + +Removes a single token from a group. + + + +```graphql +mutation RemoveTokenFromGroup { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + removeTokenFromGroup: { + collectionId: 3484 + tokenId: 107335160659631965554526087350803497388 + groupId: 60 + } + } + ) { + uuid + action + state + } +} +``` + + + +## setTokenGroups + +Replaces the full set of groups a token belongs to. Pass `groupIds: []` to remove the token from every group at once. + + + +```graphql +mutation SetTokenGroups { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + setTokenGroups: { + collectionId: 3484 + tokenId: 107335160659631965554526087350803497388 + groupIds: [60, 63] + } + } + ) { + uuid + action + state + } +} +``` + + + +## setTokenGroupAttribute + +Set or update a single key/value attribute on a token group. The collection-and-group identification uses just `id` (the token-group id), not a (collectionId, groupId) pair. + + + +```graphql +mutation SetTokenGroupAttribute { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + setTokenGroupAttribute: { + id: 60 + key: "uri" + value: "https://example.com/metadata/token-groups/clan-tag.json" + } + } + ) { + uuid + action + state + } +} +``` + + + +## removeTokenGroupAttribute + +Remove a single attribute from a token group. + + + +```graphql +mutation RemoveTokenGroupAttribute { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + removeTokenGroupAttribute: { + id: 60 + key: "uri" + } + } + ) { + uuid + action + state + } +} +``` + + + +## Adding a token to a group at creation time + +When you create a new token via [`createToken`](/03-api-reference/02-mutations/03-tokens-mutations.md#createtoken), you can include `groups: [...]` in the input to add it to one or more existing groups immediately — no follow-up `addTokenToGroup` call needed. + +```graphql +transaction: { + createToken: { + recipient: "efRecipientAddress" + collectionId: 3484 + tokenId: 12345 + initialSupply: 1 + listingForbidden: false + infusion: 0 + anyoneCanInfuse: false + groups: [60] + } +} +``` diff --git a/docs/03-api-reference/02-mutations/08-nomination-pool-mutations.md b/docs/03-api-reference/02-mutations/08-nomination-pool-mutations.md new file mode 100644 index 0000000..56c7458 --- /dev/null +++ b/docs/03-api-reference/02-mutations/08-nomination-pool-mutations.md @@ -0,0 +1,112 @@ +--- +title: "Nomination Pools" +slug: "nomination-pools" +description: "Bond and unbond ENJ in Relaychain nomination pools via CreateTransaction." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::info Please note: This is an introductory reference +For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ +🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. +::: + +:::tip GraphQL Endpoint +`https://platform.beta.enjin.io/graphql` +::: + +Nomination pools live on the **Relaychain** — pass `chain: RELAY` on every mutation in this section. To read pool state, see [Nomination Pool Queries](/03-api-reference/01-queries/08-nomination-pool-queries.md). + +Both actions below are submitted through [`CreateTransaction`](/03-api-reference/02-mutations/01-transaction-mutations.md#createtransaction), with the action selected by the field set on the `transaction` input. The response is always a `Transaction`. + +## nominationPoolsBond + +Bonds ENJ into a pool. Provide either `amount` (a specific quantity, in the smallest ENJ unit) **or** `fill: true` (bond the signer's entire free balance). + + + +```graphql +mutation NominationPoolsBond { + CreateTransaction( + network: ENJIN + chain: RELAY + transaction: { + nominationPoolsBond: { + id: 1 + amount: 5000000000000000000000 # 5,000 ENJ + } + } + ) { + uuid + action + state + } +} +``` + + +```graphql +mutation NominationPoolsBondFill { + CreateTransaction( + network: ENJIN + chain: RELAY + transaction: { + nominationPoolsBond: { + id: 1 + fill: true + } + } + ) { + uuid + action + state + } +} +``` + + +```json +{ + "data": { + "CreateTransaction": { + "uuid": "a90ded41-4262-40a2-95c0-98255b660bf1", + "action": "NominationPools.bond", + "state": "PENDING" + } + } +} +``` + + + +The pool must be in the `OPEN` state — see [`GetNominationPool.state`](/03-api-reference/01-queries/08-nomination-pool-queries.md#getnominationpool). + +## nominationPoolsUnbond + +Unbonds a quantity of pool "points" from a pool the signer is a member of. Pool points are an internal accounting unit; the equivalent ENJ amount becomes spendable after the chain's unbonding period. + + + +```graphql +mutation NominationPoolsUnbond { + CreateTransaction( + network: ENJIN + chain: RELAY + transaction: { + nominationPoolsUnbond: { + id: 1 + unbondingPoints: 5000000000000000000000 + } + } + ) { + uuid + action + state + } +} +``` + + + +The unbonded amount appears in [`GetAccountPools.totalUnbonding`](/03-api-reference/01-queries/08-nomination-pool-queries.md#getaccountpools) until the unbonding period elapses. diff --git a/docs/03-api-reference/03-api-reference.md b/docs/03-api-reference/03-api-reference.md index f2a7244..2c81659 100644 --- a/docs/03-api-reference/03-api-reference.md +++ b/docs/03-api-reference/03-api-reference.md @@ -11,8 +11,8 @@ This section is a per-resource reference for the Enjin Platform GraphQL API. Eac ## How this section is organised -- **Queries** — read on-chain and platform state: [transactions](/03-api-reference/01-queries/01-transactions-queries.md), [collections](/03-api-reference/01-queries/02-collections-queries.md), [tokens](/03-api-reference/01-queries/03-tokens-queries.md), [accounts](/03-api-reference/01-queries/04-wallets-queries.md), [fuel tanks](/03-api-reference/01-queries/05-fuel-tank-queries.md), [marketplace](/03-api-reference/01-queries/06-marketplace-queries.md). -- **Mutations** — submit on-chain actions and manage platform-side resources: [transactions](/03-api-reference/02-mutations/01-transaction-mutations.md), [collections](/03-api-reference/02-mutations/02-collections-mutations.md), [tokens](/03-api-reference/02-mutations/03-tokens-mutations.md), [wallets](/03-api-reference/02-mutations/04-wallets-mutations.md), [fuel tanks](/03-api-reference/02-mutations/05-fuel-tanks-mutations.md), [marketplace](/03-api-reference/02-mutations/06-marketplace-mutations.md). +- **Queries** — read on-chain and platform state: [transactions](/03-api-reference/01-queries/01-transactions-queries.md), [collections](/03-api-reference/01-queries/02-collections-queries.md), [tokens](/03-api-reference/01-queries/03-tokens-queries.md), [accounts](/03-api-reference/01-queries/04-wallets-queries.md), [fuel tanks](/03-api-reference/01-queries/05-fuel-tank-queries.md), [marketplace](/03-api-reference/01-queries/06-marketplace-queries.md), [token groups](/03-api-reference/01-queries/07-token-groups-queries.md), [nomination pools](/03-api-reference/01-queries/08-nomination-pool-queries.md). +- **Mutations** — submit on-chain actions and manage platform-side resources: [transactions](/03-api-reference/02-mutations/01-transaction-mutations.md), [collections](/03-api-reference/02-mutations/02-collections-mutations.md), [tokens](/03-api-reference/02-mutations/03-tokens-mutations.md), [wallets](/03-api-reference/02-mutations/04-wallets-mutations.md), [fuel tanks](/03-api-reference/02-mutations/05-fuel-tanks-mutations.md), [marketplace](/03-api-reference/02-mutations/06-marketplace-mutations.md), [token groups](/03-api-reference/02-mutations/07-token-groups-mutations.md), [nomination pools](/03-api-reference/02-mutations/08-nomination-pool-mutations.md). - [**Important Arguments**](/03-api-reference/04-important-arguments.md) — a glossary of recurring argument types and enum values (`network`, `chain`, `state`, `cap`, etc.) used throughout the API. If you're new to GraphQL or to the Enjin API, start with [Using the Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) and the [How to Use GraphQL](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md) primer — they cover authentication, the playground, and operation structure. From 58acc5ebcf03398684816575101b379766abc8ed Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 25 May 2026 19:00:15 +0300 Subject: [PATCH 43/67] Drop the \"introductory reference\" callout from API reference pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v2 callout warned readers that the page might lag behind the real schema because the docs couldn't be programmatically updated. That caveat no longer applies — these pages are maintained directly against the live API as it evolves, so the disclaimer just adds noise above every page's first real content. --- docs/03-api-reference/01-queries/01-transactions-queries.md | 5 ----- docs/03-api-reference/01-queries/02-collections-queries.md | 5 ----- docs/03-api-reference/01-queries/03-tokens-queries.md | 5 ----- docs/03-api-reference/01-queries/04-wallets-queries.md | 5 ----- docs/03-api-reference/01-queries/05-fuel-tank-queries.md | 5 ----- docs/03-api-reference/01-queries/06-marketplace-queries.md | 5 ----- docs/03-api-reference/01-queries/07-token-groups-queries.md | 5 ----- .../01-queries/08-nomination-pool-queries.md | 5 ----- .../02-mutations/01-transaction-mutations.md | 5 ----- .../02-mutations/02-collections-mutations.md | 5 ----- docs/03-api-reference/02-mutations/03-tokens-mutations.md | 5 ----- docs/03-api-reference/02-mutations/04-wallets-mutations.md | 5 ----- .../03-api-reference/02-mutations/05-fuel-tanks-mutations.md | 5 ----- .../02-mutations/06-marketplace-mutations.md | 5 ----- .../02-mutations/07-token-groups-mutations.md | 5 ----- .../02-mutations/08-nomination-pool-mutations.md | 5 ----- docs/03-api-reference/04-important-arguments.md | 5 ----- 17 files changed, 85 deletions(-) diff --git a/docs/03-api-reference/01-queries/01-transactions-queries.md b/docs/03-api-reference/01-queries/01-transactions-queries.md index f729993..d82e70a 100644 --- a/docs/03-api-reference/01-queries/01-transactions-queries.md +++ b/docs/03-api-reference/01-queries/01-transactions-queries.md @@ -8,11 +8,6 @@ import GlossaryTerm from '@site/src/components/GlossaryTerm'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - :::tip GraphQL Endpoint `https://platform.beta.enjin.io/graphql` ::: diff --git a/docs/03-api-reference/01-queries/02-collections-queries.md b/docs/03-api-reference/01-queries/02-collections-queries.md index 5ba6630..3d9d3e4 100644 --- a/docs/03-api-reference/01-queries/02-collections-queries.md +++ b/docs/03-api-reference/01-queries/02-collections-queries.md @@ -7,11 +7,6 @@ description: "Query the Enjin API for collections: metadata, policies, token gro import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - :::tip GraphQL Endpoint `https://platform.beta.enjin.io/graphql` ::: diff --git a/docs/03-api-reference/01-queries/03-tokens-queries.md b/docs/03-api-reference/01-queries/03-tokens-queries.md index 82ea0d3..fd4c145 100644 --- a/docs/03-api-reference/01-queries/03-tokens-queries.md +++ b/docs/03-api-reference/01-queries/03-tokens-queries.md @@ -7,11 +7,6 @@ description: "Query the Enjin API for tokens: supply, cap, behavior, attributes, import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - :::tip GraphQL Endpoint `https://platform.beta.enjin.io/graphql` ::: diff --git a/docs/03-api-reference/01-queries/04-wallets-queries.md b/docs/03-api-reference/01-queries/04-wallets-queries.md index 1a546c0..2aad061 100644 --- a/docs/03-api-reference/01-queries/04-wallets-queries.md +++ b/docs/03-api-reference/01-queries/04-wallets-queries.md @@ -7,11 +7,6 @@ description: "Read on-chain accounts, managed wallets, and verify signed message import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - :::tip GraphQL Endpoint `https://platform.beta.enjin.io/graphql` ::: diff --git a/docs/03-api-reference/01-queries/05-fuel-tank-queries.md b/docs/03-api-reference/01-queries/05-fuel-tank-queries.md index 4865355..51570ca 100644 --- a/docs/03-api-reference/01-queries/05-fuel-tank-queries.md +++ b/docs/03-api-reference/01-queries/05-fuel-tank-queries.md @@ -7,11 +7,6 @@ description: "Query the Enjin API for fuel tanks data, which helps cover transac import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - :::tip GraphQL Endpoint `https://platform.beta.enjin.io/graphql` ::: diff --git a/docs/03-api-reference/01-queries/06-marketplace-queries.md b/docs/03-api-reference/01-queries/06-marketplace-queries.md index 3558961..f4b8db5 100644 --- a/docs/03-api-reference/01-queries/06-marketplace-queries.md +++ b/docs/03-api-reference/01-queries/06-marketplace-queries.md @@ -7,11 +7,6 @@ description: "Query the Enjin API for marketplace listings." import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - :::tip GraphQL Endpoint `https://platform.beta.enjin.io/graphql` ::: diff --git a/docs/03-api-reference/01-queries/07-token-groups-queries.md b/docs/03-api-reference/01-queries/07-token-groups-queries.md index af9cea2..a8cf673 100644 --- a/docs/03-api-reference/01-queries/07-token-groups-queries.md +++ b/docs/03-api-reference/01-queries/07-token-groups-queries.md @@ -7,11 +7,6 @@ description: "Read token groups via Collection.tokenGroups and Token.tokenGroupT import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - :::tip GraphQL Endpoint `https://platform.beta.enjin.io/graphql` ::: diff --git a/docs/03-api-reference/01-queries/08-nomination-pool-queries.md b/docs/03-api-reference/01-queries/08-nomination-pool-queries.md index b964465..aa2fb11 100644 --- a/docs/03-api-reference/01-queries/08-nomination-pool-queries.md +++ b/docs/03-api-reference/01-queries/08-nomination-pool-queries.md @@ -7,11 +7,6 @@ description: "Read Relaychain nomination pools and an account's pool memberships import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - :::tip GraphQL Endpoint `https://platform.beta.enjin.io/graphql` ::: diff --git a/docs/03-api-reference/02-mutations/01-transaction-mutations.md b/docs/03-api-reference/02-mutations/01-transaction-mutations.md index 82087fa..c8b791d 100644 --- a/docs/03-api-reference/02-mutations/01-transaction-mutations.md +++ b/docs/03-api-reference/02-mutations/01-transaction-mutations.md @@ -7,11 +7,6 @@ description: "Submit on-chain actions through the Enjin Platform's transaction m import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - :::tip GraphQL Endpoint `https://platform.beta.enjin.io/graphql` ::: diff --git a/docs/03-api-reference/02-mutations/02-collections-mutations.md b/docs/03-api-reference/02-mutations/02-collections-mutations.md index 3ee1780..2d4a962 100644 --- a/docs/03-api-reference/02-mutations/02-collections-mutations.md +++ b/docs/03-api-reference/02-mutations/02-collections-mutations.md @@ -7,11 +7,6 @@ description: "Create, mutate, freeze, transfer, and destroy on-chain collections import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - :::tip GraphQL Endpoint `https://platform.beta.enjin.io/graphql` ::: diff --git a/docs/03-api-reference/02-mutations/03-tokens-mutations.md b/docs/03-api-reference/02-mutations/03-tokens-mutations.md index 1c7ee4c..6061a89 100644 --- a/docs/03-api-reference/02-mutations/03-tokens-mutations.md +++ b/docs/03-api-reference/02-mutations/03-tokens-mutations.md @@ -7,11 +7,6 @@ description: "Create, mint, transfer, burn, freeze, infuse, and attribute tokens import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - :::tip GraphQL Endpoint `https://platform.beta.enjin.io/graphql` ::: diff --git a/docs/03-api-reference/02-mutations/04-wallets-mutations.md b/docs/03-api-reference/02-mutations/04-wallets-mutations.md index ae87768..bfc8816 100644 --- a/docs/03-api-reference/02-mutations/04-wallets-mutations.md +++ b/docs/03-api-reference/02-mutations/04-wallets-mutations.md @@ -7,11 +7,6 @@ description: "Create platform-managed wallets via the Enjin API." import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - :::tip GraphQL Endpoint `https://platform.beta.enjin.io/graphql` ::: diff --git a/docs/03-api-reference/02-mutations/05-fuel-tanks-mutations.md b/docs/03-api-reference/02-mutations/05-fuel-tanks-mutations.md index 7ebbc05..c6bface 100644 --- a/docs/03-api-reference/02-mutations/05-fuel-tanks-mutations.md +++ b/docs/03-api-reference/02-mutations/05-fuel-tanks-mutations.md @@ -7,11 +7,6 @@ description: "Use the Enjin API to perform fuel tanks mutations, including setti import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - :::tip GraphQL Endpoint `https://platform.beta.enjin.io/graphql` ::: diff --git a/docs/03-api-reference/02-mutations/06-marketplace-mutations.md b/docs/03-api-reference/02-mutations/06-marketplace-mutations.md index 6a31277..e1b6fea 100644 --- a/docs/03-api-reference/02-mutations/06-marketplace-mutations.md +++ b/docs/03-api-reference/02-mutations/06-marketplace-mutations.md @@ -7,11 +7,6 @@ description: "Create, fill, cancel, finalize, bid, counter-offer, and whitelist import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - :::tip GraphQL Endpoint `https://platform.beta.enjin.io/graphql` ::: diff --git a/docs/03-api-reference/02-mutations/07-token-groups-mutations.md b/docs/03-api-reference/02-mutations/07-token-groups-mutations.md index 25f865e..f124ac2 100644 --- a/docs/03-api-reference/02-mutations/07-token-groups-mutations.md +++ b/docs/03-api-reference/02-mutations/07-token-groups-mutations.md @@ -7,11 +7,6 @@ description: "Create, destroy, add/remove tokens to, and attribute on-chain toke import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - :::tip GraphQL Endpoint `https://platform.beta.enjin.io/graphql` ::: diff --git a/docs/03-api-reference/02-mutations/08-nomination-pool-mutations.md b/docs/03-api-reference/02-mutations/08-nomination-pool-mutations.md index 56c7458..64204b8 100644 --- a/docs/03-api-reference/02-mutations/08-nomination-pool-mutations.md +++ b/docs/03-api-reference/02-mutations/08-nomination-pool-mutations.md @@ -7,11 +7,6 @@ description: "Bond and unbond ENJ in Relaychain nomination pools via CreateTrans import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - :::tip GraphQL Endpoint `https://platform.beta.enjin.io/graphql` ::: diff --git a/docs/03-api-reference/04-important-arguments.md b/docs/03-api-reference/04-important-arguments.md index b11551d..11a0219 100644 --- a/docs/03-api-reference/04-important-arguments.md +++ b/docs/03-api-reference/04-important-arguments.md @@ -6,11 +6,6 @@ description: "A quick reference for important arguments and enum values used thr import GlossaryTerm from '@site/src/components/GlossaryTerm'; -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - A glossary of recurring argument types, fields, and enum values you'll see across the Enjin API. ## network From 3c531fd64188f213000789b6e9b2a8026e84d0e8 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 25 May 2026 20:08:54 +0300 Subject: [PATCH 44/67] Use Enjin's The Multiverse / Epochrome Sword as the canonical token-group example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap the [MvB] Clan Tag / Multiverse Brotherhood references (a community collection) for The Multiverse (id 2967) and its Epochrome Sword group (id 694) — both Enjin-official, so the examples read as canonical. Real collection owner address, real listing id and prices, and a real token id from the Epochrome Sword group are all verified live on mainnet Matrix. Touched: 07-token-groups-queries, 07-token-groups-mutations, 02-collections-queries, 02-collections-mutations, 06-marketplace-queries, 04-wallets-queries. --- .../01-queries/02-collections-queries.md | 23 +++++----- .../01-queries/04-wallets-queries.md | 8 ++-- .../01-queries/06-marketplace-queries.md | 42 +++++++++---------- .../01-queries/07-token-groups-queries.md | 32 +++++++------- .../02-mutations/02-collections-mutations.md | 2 +- .../02-mutations/07-token-groups-mutations.md | 32 +++++++------- 6 files changed, 70 insertions(+), 69 deletions(-) diff --git a/docs/03-api-reference/01-queries/02-collections-queries.md b/docs/03-api-reference/01-queries/02-collections-queries.md index 3d9d3e4..82bac5a 100644 --- a/docs/03-api-reference/01-queries/02-collections-queries.md +++ b/docs/03-api-reference/01-queries/02-collections-queries.md @@ -21,7 +21,7 @@ Returns a single collection by id. ```graphql query GetCollection { - GetCollection(network: ENJIN, chain: MATRIX, id: 3484) { + GetCollection(network: ENJIN, chain: MATRIX, id: 2967) { id owner { address @@ -66,14 +66,14 @@ query GetCollection { { "data": { "GetCollection": { - "id": "3484", + "id": "2967", "owner": { - "address": "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y" + "address": "efPvNL3eGKd82cwQ9SeV1fyuiwDMM5wkrW5EoAPF42kvDJgG1" }, "attributes": [], "metadata": { - "name": "Multiverse Brotherhood", - "description": null + "name": "The Multiverse", + "description": "Use these items in 10+ games. Find out more at enjin.io/multiverse" }, "mintPolicy": { "forceCollapsingSupply": false, @@ -88,8 +88,9 @@ query GetCollection { }, "pendingTransfer": null, "tokenGroups": [ - { "id": "60", "metadata": { "name": "[MvB] Clan Tag" } }, - { "id": "63", "metadata": { "name": "Multiverse Brotherhood ©" } } + { "id": "694", "metadata": { "name": "Epochrome Sword" } }, + { "id": "695", "metadata": { "name": "Pegasoid Steed" } }, + { "id": "700", "metadata": { "name": "Soulshift Armor" } } ] } } @@ -111,7 +112,7 @@ query GetCollections { GetCollections( network: ENJIN chain: MATRIX - ids: [3484, 7153] + ids: [2967, 7153] ) { id owner { @@ -134,9 +135,9 @@ query GetCollections { "data": { "GetCollections": [ { - "id": "3484", - "owner": { "address": "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y" }, - "metadata": { "name": "Multiverse Brotherhood" }, + "id": "2967", + "owner": { "address": "efPvNL3eGKd82cwQ9SeV1fyuiwDMM5wkrW5EoAPF42kvDJgG1" }, + "metadata": { "name": "The Multiverse" }, "mintPolicy": { "forceCollapsingSupply": false, "maxTokenCount": null } }, { diff --git a/docs/03-api-reference/01-queries/04-wallets-queries.md b/docs/03-api-reference/01-queries/04-wallets-queries.md index 2aad061..e15cb84 100644 --- a/docs/03-api-reference/01-queries/04-wallets-queries.md +++ b/docs/03-api-reference/01-queries/04-wallets-queries.md @@ -61,11 +61,11 @@ query GetAccount { "balance": "17500000000000000000", "tokens": [ { - "id": "3484-2", - "tokenId": "2", + "id": "2967-107002853660685728525072975374659356720", + "tokenId": "107002853660685728525072975374659356720", "isNonFungible": false, - "collection": { "id": "3484" }, - "metadata": { "name": "[MvB] Sigil" } + "collection": { "id": "2967" }, + "metadata": { "name": "Epochrome Sword" } } ] } diff --git a/docs/03-api-reference/01-queries/06-marketplace-queries.md b/docs/03-api-reference/01-queries/06-marketplace-queries.md index f4b8db5..15fb028 100644 --- a/docs/03-api-reference/01-queries/06-marketplace-queries.md +++ b/docs/03-api-reference/01-queries/06-marketplace-queries.md @@ -24,7 +24,7 @@ query GetListing { GetListing( network: ENJIN chain: MATRIX - id: "8bd40957579f61b8a04ddaf7c86fbd51329e88f5ec735e910a320169377db161" + id: "2cabb4256b0806c68e53104769e3ba7382b6f10e37df7bceb74a6119f601bf3d" ) { id seller { @@ -52,15 +52,15 @@ query GetListing { { "data": { "GetListing": { - "id": "8bd40957579f61b8a04ddaf7c86fbd51329e88f5ec735e910a320169377db161", - "seller": { "address": "efRCJAEyn5Rp7SX9gvoJEAG7y1td3PZzRhpJYEeofpGVj3ouc" }, - "makeAsset": { "id": "3484-2", "tokenId": "2" }, + "id": "2cabb4256b0806c68e53104769e3ba7382b6f10e37df7bceb74a6119f601bf3d", + "seller": { "address": "efS3yL43XUmqwU2ovUZjjdzfo4eQk5dR7pbUf4dUFxPxqKh2V" }, + "makeAsset": { "id": "2967-106338239662793367710728619925308440576", "tokenId": "106338239662793367710728619925308440576" }, "takeAsset": null, - "amount": "72", - "price": "12000000000000000000", - "minTakeValue": "807840000000000000000", + "amount": "2", + "price": "620000000000000000000", + "minTakeValue": "1209000000000000000000", "isActive": true, - "highestPrice": "12000000000000000000" + "highestPrice": "620000000000000000000" } } } @@ -85,7 +85,7 @@ query GetListings { GetListings( network: ENJIN chain: MATRIX - collectionId: 3484 + collectionId: 2967 limit: 15 page: 1 ) { @@ -109,19 +109,19 @@ query GetListings { "data": { "GetListings": [ { - "id": "8bd40957579f61b8a04ddaf7c86fbd51329e88f5ec735e910a320169377db161", - "seller": { "address": "efRCJAEyn5Rp7SX9gvoJEAG7y1td3PZzRhpJYEeofpGVj3ouc" }, - "makeAsset": { "tokenId": "2" }, - "amount": "72", - "price": "12000000000000000000", + "id": "2cabb4256b0806c68e53104769e3ba7382b6f10e37df7bceb74a6119f601bf3d", + "seller": { "address": "efS3yL43XUmqwU2ovUZjjdzfo4eQk5dR7pbUf4dUFxPxqKh2V" }, + "makeAsset": { "tokenId": "106338239662793367710728619925308440576" }, + "amount": "2", + "price": "620000000000000000000", "isActive": true }, { - "id": "f3e76d1413e30548204dca8d0a9559dc981ce60e70b49fca4d031f7b7842a3c4", - "seller": { "address": "efUfmhm2rcTtVr4ajneB71qbfDZ1EkyYW7xRc6fhuRh3Tg4L1" }, - "makeAsset": { "tokenId": "4" }, - "amount": "3", - "price": "55000000000000000000", + "id": "41fbe28706d544a1afc39d894723d98d8e7922a6be77a8545af23ec2fb998341", + "seller": { "address": "efTwg56JrsGcDomiouuTfPybQuRNYX53XBHe6JrgxMvdDtkFw" }, + "makeAsset": { "tokenId": "107002853660685728525072975374659359309" }, + "amount": "1", + "price": "77000000000000000000", "isActive": true } ] @@ -138,8 +138,8 @@ query GetListings { GetListings( network: ENJIN chain: MATRIX - collectionId: 3484 - tokenIds: [2, 4] + collectionId: 2967 + tokenIds: [106338239662793367710728619925308440576, 107002853660685728525072975374659359309] limit: 15 page: 1 ) { diff --git a/docs/03-api-reference/01-queries/07-token-groups-queries.md b/docs/03-api-reference/01-queries/07-token-groups-queries.md index a8cf673..cb3facd 100644 --- a/docs/03-api-reference/01-queries/07-token-groups-queries.md +++ b/docs/03-api-reference/01-queries/07-token-groups-queries.md @@ -11,7 +11,7 @@ import TabItem from '@theme/TabItem'; `https://platform.beta.enjin.io/graphql` ::: -A **token group** is an on-chain set of tokens within a single collection. Each token can belong to zero or more groups; each group can hold many tokens. Groups make it easier to organise large collections in marketplaces and games — for example, you might create a "Legendary Swords" group inside a `Multiverse Brotherhood` collection and grant in-game utility to anyone holding a token from that group. +A **token group** is an on-chain set of tokens within a single collection. Each token can belong to zero or more groups; each group can hold many tokens. Groups make it easier to organise large collections in marketplaces and games — for example, the `Epochrome Sword` group inside Enjin's `The Multiverse` collection bundles every Epochrome Sword variant together so games can grant in-game utility to anyone holding any one of them. There is no dedicated `GetTokenGroup` root query. Read groups via the two sides they're related to: @@ -28,7 +28,7 @@ Use [`GetCollection`](/03-api-reference/01-queries/02-collections-queries.md#get ```graphql query GetCollectionGroups { - GetCollection(network: ENJIN, chain: MATRIX, id: 3484) { + GetCollection(network: ENJIN, chain: MATRIX, id: 2967) { id tokenGroups { id @@ -54,20 +54,20 @@ query GetCollectionGroups { { "data": { "GetCollection": { - "id": "3484", + "id": "2967", "tokenGroups": [ { - "id": "60", + "id": "694", "attributes": [ - { "key": "uri", "value": "https://example.com/metadata/token-groups/clan-tag.json" } + { "key": "uri", "value": "https://example.com/metadata/token-groups/epochrome-sword.json" } ], "metadata": { - "name": "[MvB] Clan Tag", + "name": "Epochrome Sword", "description": null }, "tokenGroupTokens": [ - { "tokenId": "3484-107335160659631965554526087350803497388", "position": 0 }, - { "tokenId": "3484-107335160659631965554526087350803497398", "position": 0 } + { "tokenId": "2967-107002853660685728525072975374659356720", "position": 0 }, + { "tokenId": "2967-107002853660685728525072975374659356721", "position": 0 } ] } ] @@ -95,7 +95,7 @@ query GetTokenGroups { GetToken( network: ENJIN chain: MATRIX - id: "3484-107335160659631965554526087350803497388" + id: "2967-107002853660685728525072975374659356720" ) { id tokenId @@ -118,16 +118,16 @@ query GetTokenGroups { { "data": { "GetToken": { - "id": "3484-107335160659631965554526087350803497388", - "tokenId": "107335160659631965554526087350803497388", + "id": "2967-107002853660685728525072975374659356720", + "tokenId": "107002853660685728525072975374659356720", "tokenGroupTokens": [ { - "tokenGroupId": "60", + "tokenGroupId": "694", "position": 0, "tokenGroup": { - "id": "60", + "id": "694", "metadata": { - "name": "[MvB] Clan Tag" + "name": "Epochrome Sword" } } } @@ -141,10 +141,10 @@ query GetTokenGroups { ## Checking whether a wallet holds any token from a specific group -There's no single-query path for this — the v3 API doesn't expose a per-(account, group) join. The pattern is: +There's no single-query path for this yet — the API doesn't expose a per-(account, group) join. The pattern is: 1. `GetAccount(address:)` → read the wallet's `tokens` list. 2. For each held token (or for the tokens you care about), select `tokenGroupTokens { tokenGroupId }`. 3. Check whether the target group id appears. -Alternatively, since the platform may evolve this surface, you can read [`Collection.tokenGroups[].tokenGroupTokens`](#reading-all-groups-in-a-collection) once and cache it, then intersect against the wallet's token ids. +Alternatively, you can read [`Collection.tokenGroups[].tokenGroupTokens`](#reading-all-groups-in-a-collection) once and cache it, then intersect against the wallet's token ids. diff --git a/docs/03-api-reference/02-mutations/02-collections-mutations.md b/docs/03-api-reference/02-mutations/02-collections-mutations.md index 2d4a962..bb98124 100644 --- a/docs/03-api-reference/02-mutations/02-collections-mutations.md +++ b/docs/03-api-reference/02-mutations/02-collections-mutations.md @@ -35,7 +35,7 @@ mutation CreateCollection { { address: "efRoyaltyBeneficiary", percentage: 5.0 } ] attributes: [ - { key: "name", value: "Multiverse Brotherhood" } + { key: "name", value: "The Multiverse" } { key: "uri", value: "https://example.com/metadata/collection.json" } ] explicitRoyaltyCurrencies: [ diff --git a/docs/03-api-reference/02-mutations/07-token-groups-mutations.md b/docs/03-api-reference/02-mutations/07-token-groups-mutations.md index f124ac2..92b0619 100644 --- a/docs/03-api-reference/02-mutations/07-token-groups-mutations.md +++ b/docs/03-api-reference/02-mutations/07-token-groups-mutations.md @@ -25,7 +25,7 @@ mutation CreateTokenGroup { network: ENJIN chain: MATRIX transaction: { - createTokenGroup: { collectionId: 3484 } + createTokenGroup: { collectionId: 2967 } } ) { uuid @@ -62,7 +62,7 @@ mutation DestroyTokenGroup { network: ENJIN chain: MATRIX transaction: { - destroyTokenGroup: { id: 60 } + destroyTokenGroup: { id: 694 } } ) { uuid @@ -87,9 +87,9 @@ mutation AddTokenToGroup { chain: MATRIX transaction: { addTokenToGroup: { - collectionId: 3484 - tokenId: 107335160659631965554526087350803497388 - groupId: 60 + collectionId: 2967 + tokenId: 107002853660685728525072975374659356720 + groupId: 694 } } ) { @@ -115,9 +115,9 @@ mutation RemoveTokenFromGroup { chain: MATRIX transaction: { removeTokenFromGroup: { - collectionId: 3484 - tokenId: 107335160659631965554526087350803497388 - groupId: 60 + collectionId: 2967 + tokenId: 107002853660685728525072975374659356720 + groupId: 694 } } ) { @@ -143,9 +143,9 @@ mutation SetTokenGroups { chain: MATRIX transaction: { setTokenGroups: { - collectionId: 3484 - tokenId: 107335160659631965554526087350803497388 - groupIds: [60, 63] + collectionId: 2967 + tokenId: 107002853660685728525072975374659356720 + groupIds: [694, 692] } } ) { @@ -171,9 +171,9 @@ mutation SetTokenGroupAttribute { chain: MATRIX transaction: { setTokenGroupAttribute: { - id: 60 + id: 694 key: "uri" - value: "https://example.com/metadata/token-groups/clan-tag.json" + value: "https://example.com/metadata/token-groups/epochrome-sword.json" } } ) { @@ -199,7 +199,7 @@ mutation RemoveTokenGroupAttribute { chain: MATRIX transaction: { removeTokenGroupAttribute: { - id: 60 + id: 694 key: "uri" } } @@ -221,13 +221,13 @@ When you create a new token via [`createToken`](/03-api-reference/02-mutations/0 transaction: { createToken: { recipient: "efRecipientAddress" - collectionId: 3484 + collectionId: 2967 tokenId: 12345 initialSupply: 1 listingForbidden: false infusion: 0 anyoneCanInfuse: false - groups: [60] + groups: [694] } } ``` From 6e9b48fb9eff2364c54f248948eeaa5fa3593153 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Tue, 26 May 2026 12:50:57 +0300 Subject: [PATCH 45/67] Stub websocket-events and working-with-events pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both pages were carrying v2 Pusher infrastructure (pusher:subscribe payloads, GetPendingEvents, dedicated WS URLs per network) that has no v3 equivalent. The Enjin team has confirmed real-time event streaming and an events field on Transaction are both planned but neither has a finalized shape yet, so writing a v3-shaped reference now would just be made up. Replace each page with a short "coming soon" callout plus the only workflow that actually works today: poll GetTransaction(uuid:) for state, then click Extrinsic Hash on the Transactions page to read emitted events on Subscan. The api-reference stub additionally lists concrete use cases the real-time API will unlock (chain-assigned IDs from createListing / createCollection, real-time token activity, state UI without polling) so the page is informative rather than just a placeholder. Also drop the now-misleading "Listen to Platform Websocket Events" subsection from section 6 of the platform getting-started page — it described a feature that isn't shipped. Replace the bullet with a single forward-pointer to the WebSocket Events stub so readers who go looking still land on the right "coming soon" explanation. --- .../04-using-the-enjin-platform.md | 9 +- docs/03-api-reference/03-websocket-events.md | 45 ++--- .../03-working-with-events.md | 177 ++---------------- 3 files changed, 29 insertions(+), 202 deletions(-) diff --git a/docs/01-getting-started/04-using-the-enjin-platform.md b/docs/01-getting-started/04-using-the-enjin-platform.md index 989dc5b..5b5542f 100644 --- a/docs/01-getting-started/04-using-the-enjin-platform.md +++ b/docs/01-getting-started/04-using-the-enjin-platform.md @@ -272,11 +272,12 @@ The API equivalent of the Batch Queue is the `CreateBatchTransaction` mutation ## 6. Receive Transaction Information -There are 3 ways to receive the transaction status and information: +There are two ways to receive the transaction status and information: - [Receive Transaction Information Using the Platform User Interface](#receive-transaction-information-using-the-platform-user-interface) - [Receive Transaction Information Using the Enjin API / SDKs](#receive-transaction-information-using-the-enjin-api--sdks) -- [Receive Transaction Information by Listening to Platform Websocket Events](#receive-transaction-information-by-listening-to-platform-websocket-events) + +For real-time, push-based notifications, see [WebSocket Events](/03-api-reference/03-websocket-events.md) — planned, not yet available. ### Receive Transaction Information Using the Platform User Interface @@ -432,10 +433,6 @@ A `state` of `FINALIZED` means the transaction has been included in a finalized For the full list of platform states, check the [Enjin API → Important Arguments](/03-api-reference/04-important-arguments.md#state) page. ::: -### Receive Transaction Information by Listening to Platform Websocket Events - -You can also subscribe to events via WebSocket — useful when you want to react to transaction state changes without polling. For more information, check out the [Websocket Events](/05-enjin-platform/03-working-with-events.md) guide. - :::tip What's Next? Learn how to [Use the API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md). ::: diff --git a/docs/03-api-reference/03-websocket-events.md b/docs/03-api-reference/03-websocket-events.md index f2325ac..ed49af4 100644 --- a/docs/03-api-reference/03-websocket-events.md +++ b/docs/03-api-reference/03-websocket-events.md @@ -1,41 +1,22 @@ --- -title: "Websocket Events" +title: "WebSocket Events" slug: "websocket-events" +description: "Real-time event streaming via the Enjin Platform — planned, not yet available." --- -:::info Websocket URLs -Enjin Platform Cloud **Canary**: `wss://ws-us2.pusher.com:443/app/76b7604dabc23c62be75?protocol=7`\ -Enjin Platform Cloud **Mainnet**: `wss://ws-us2.pusher.com:443/app/02cbd93e7842fb1db299?protocol=7` -::: - -The Enjin Platform emits events that you can listen to for various operations. -For example, whenever a transaction is issued by your wallet daemon, an event is emitted via Websocket. - -Listening to events is especially useful when awaiting for a transaction to be approved on-chain. -Platform events are emitted to the platform Websocket endpoint, in various channels. -For example, we can listen to events related to account address `cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f` by subscribing to the channel `0x5a6aae294416f3e875d9a8975658905002cfd3e5e64105d76296c4b0adbfd77e`, which is the account's public key. - -:::note Note on Address Formats in Platform Events -Please note that all account addresses included in platform events, such as channel names or any addresses within the event payload, are formatted as **public keys**.\ -For example, in a "Transfer" event, the "from" account address appears in its public key format.\ -If needed, you may use the [Account Format Transform tool](https://matrix.subscan.io/tools/format_transform) to convert between **SS58-encoded address format** (e.g., "cx...123" for the Canary Matrixchain or "cn...123" for the Canary Relaychain) and public key format. +:::warning Coming soon +Real-time event streaming is **planned** for the Enjin Platform but is **not yet available**. The exact API shape (subscription endpoint, channel structure, event payload) hasn't been finalized — this page will be filled in with the full reference once support ships. ::: -```json -{ - "event":"pusher:subscribe", - "data":{ - "channel":"0x5a6aae294416f3e875d9a8975658905002cfd3e5e64105d76296c4b0adbfd77e" - } -} -``` +When it ships, this page will document how to subscribe to real-time events emitted by the platform. Typical use cases include: + +- **Reading IDs that the chain assigns at execution time** — for example, picking up the new `listing_id` the moment a `createListing` transaction is finalized, or the new `collection_id` after a `createCollection`, without having to poll or look up Subscan. +- **Reacting to incoming token activity in real time** — unlocking an in-game item the instant a transfer to the player's wallet finalizes, or refreshing a marketplace UI the moment a bid lands on an active auction. +- **Driving transaction state UI without polling** — flipping a "Pending → Broadcast → Finalized" indicator off the platform's own state changes. -![Example banner](/img/api-functions/Pusher.gif) +## Until then -To get the full list of events and the channels they are broadcasted in, send the `GetEvents` query: +- **Transaction state** — poll [`GetTransaction(uuid:)`](/03-api-reference/01-queries/01-transactions-queries.md#gettransaction) until `state` becomes `FINALIZED`. The transaction's `extrinsicHash` is also exposed once it's broadcast. +- **On-chain events emitted by a transaction** (the new collection ID after `createCollection`, the listing ID after `createListing`, etc.) — open the transaction on Subscan via its `extrinsicHash` and read the **Events** tab. See [Working with Events](/05-enjin-platform/03-working-with-events.md) for the full flow. -```graphql -query { - GetEvents -} -``` +An `events` field will also be added to the `Transaction` type so on-chain event payloads can be read alongside the rest of the transaction. Once it ships, this page and the `GetTransaction` reference will be updated with the full event shape and response examples. diff --git a/docs/05-enjin-platform/03-working-with-events.md b/docs/05-enjin-platform/03-working-with-events.md index 6b96dbf..ca6208a 100644 --- a/docs/05-enjin-platform/03-working-with-events.md +++ b/docs/05-enjin-platform/03-working-with-events.md @@ -1,177 +1,26 @@ --- title: "Working with Events" slug: "working-with-events" -description: "Explore how to handle blockchain events within the Enjin platform, enabling real-time updates and interactions in your application." +description: "How to read on-chain events emitted by transactions on the Enjin Platform." --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; +In the blockchain ecosystem, "events" are the on-chain log entries emitted by extrinsics — they're how the chain reports what actually happened: token transfers, collection creation, marketplace listing IDs, balance changes, and so on. Monitoring them is essential for any application that needs to react to state changes — for example, unlocking an in-game item the moment the user receives the corresponding NFT. -In the blockchain ecosystem, "events" refer to important actions or changes within the blockchain network. +:::warning Coming soon +Reading the on-chain events emitted by a transaction through the Enjin Platform API is **planned** but **not yet available**. An `events` field will be added to the `Transaction` type so you'll be able to read them alongside `state`, `extrinsicHash`, etc. The exact shape isn't finalized — this page will be updated with response examples once support ships. -This includes things like creating or changing digital assets, transferring balances, and any other actions that alter the chain's state. These events are recorded on the blockchain, providing a transparent and unchangeable history of all activities. - -Monitoring and analyzing these events helps users and developers understand the flow of transactions and respond to specific actions or conditions in the network. This is key for decentralized applications and games, enabling automated interactions and improving overall functionality. - -:::warning Note on Address Formats in Platform Events -Please note that all account addresses included in platform events, such as channel names or any addresses within the event payload, are formatted as **public keys**. -For example, in a "Transfer" event, the "from" account address appears in its public key format. -If needed, you may use the [Account Format Transform tool](https://matrix.subscan.io/tools/format_transform) to convert between **SS58-encoded address format** (e.g., "cx...123" for the Canary Matrixchain or "cn...123" for the Canary Relaychain) and public key format. +Real-time push-based event streaming is also planned — see [WebSocket Events](/03-api-reference/03-websocket-events.md). ::: -## Using Enjin Platform - -To view events emitted from a transaction conducted on the platform, you can use the `GetTransaction` query as follows: - - - -```graphql -query GetTransaction { - GetTransaction(id: 40000) { - method - state - result - events { - edges { - node { - moduleld - eventId - params { - type - value - } - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetTransaction": { - "method": "BatchTransfer", - "state": "FINALIZED", - "result": "EXTRINSIC_SUCCESS", - "events": { - "edges": [ - { - "node": { - "moduleId": "Balances", - "eventId": "Withdraw", - "params": [ - { - "type": "who", - "value": "f64fda4cb0352aff9394f0cc14495ca8cefffb8c8e9b37de058a1d4d4650c3313" - }, - { - "type": "amount", - "value": "20675071170417823" - } - ] - } - }, - { - "node": { - "moduleId": "MultiTokens", - "eventId": "CollectionAccountCreated", - "params": [ - { - "type": "collection_id", - "value": "2155" - }, - { - "type": "account", - "value": "303049fa4535f21acc5760521c6cc91f01ddf28ebf771da99effbf5c1031416F" - } - ] - } - } - ] - } - } - } -} -``` - - - -Our platform broadcasts network events through a `WebSocket` channel, similar to the recent events feature in Polkadot-JS. It's important to note that this channel includes not only network-wide events but also specific events from the Enjin Platform. - -Learn more about [Websocket Events here](/05-enjin-platform/03-working-with-events.md). +## Reading events today -![Platform Events](/img/guides/going-open-source/platform-events.png) +Until the `Transaction.events` field ships, the workflow is: -Our platform uses WebSockets to transmit real-time events, but since WebSockets can sometimes be unstable, there's a risk of missing events if your system loses connection. To address this, we also cache events in a query called `GetPendingEvents`. The most effective way to stay updated in real time and ensure no events are missed is to always `acknowledge` the events you receive. +1. **Poll for finalization.** Call [`GetTransaction(uuid:)`](/03-api-reference/01-queries/01-transactions-queries.md#gettransaction) until `state` is `FINALIZED`. The response also exposes `extrinsicHash` once the transaction has been broadcast. +2. **Read the events on Subscan.** Open the transaction by its `extrinsicHash` on the [Enjin Matrixchain Subscan explorer](https://matrix.subscan.io/). The **Events** tab there lists every event the extrinsic emitted, including IDs you'll commonly need (new `collection_id`, `token_id`, `listing_id`, etc.). - - -```graphql -query GetPendingEvents { - GetPendingEvents( - acknowledgeEvents: false, - first: 50 - ) { - edges { - node { - name - data - } - } - } -} -``` - - -```json -{ - "data": { - "GetPendingEvents": { - "edges": [ - { - "node": { - "name": "platform:transaction-updated", - "data": { - "id": 179419, - "state": "PROCESSING", - "method": "BatchTransfer", - "result": null, - "transactionId": null, - "idempotencyKey": "4eace652-9c9f-4721-bfe3-1be6f7597c32", - "transactionHash": null - } - } - }, - { - "node": { - "name": "platform:transaction-updated", - "data": { - "id": 179417, - "state": "PROCESSING", - "method": "BatchTransfer", - "result": null, - "transactionId": null, - "idempotencyKey": "a3e62297-983e-4a14-81a9-975976302127", - "transactionHash": null - } - } - } - ] - } - } -} -``` - - +The Enjin dashboard makes this one click — open the [Transactions page](https://platform.beta.enjin.io/transactions), find your transaction, and click its **Extrinsic Hash** link to jump straight to the Subscan event view. -## Using Enjin Console - -In the explorer tab, you can find the recent events emitted in the network. Those will appear as soon as they are emitted. - -![Viewing events on Enjin Console](/img/guides/going-open-source/enjin-console-events.png) - -You can also see the events emitted in a single block by querying the block directly - -![Viewing events on Enjin Console](/img/guides/going-open-source/enjin-console-events-2.png) +:::note Address formats in event payloads +On-chain events represent account addresses as **hex public keys**, not the SS58-encoded `ef…` / `cx…` strings shown in most platform UIs. Use Subscan's [Account Format Transform tool](https://matrix.subscan.io/tools/format_transform) to convert between the two. +::: From 92c30df7cfa7c5df56dfebf05f1a7d65e3b344f6 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Tue, 26 May 2026 13:15:41 +0300 Subject: [PATCH 46/67] Cross-link the events docs from 18 user-facing pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new Working with Events page is only useful if readers can find it from where they actually are — the create-collection guide, the marketplace mutation reference, the wallet-daemon walkthrough. Add a short forward-pointer at each natural insertion point so a reader mid-task doesn't have to know the events page exists to discover it. Two flavours of pointer. High-value events (chain assigns an ID — collection_id on createCollection, listing_id on createListing, group_id on createTokenGroup; or carries useful payload — Minted / Transferred) name the specific event and what it contains. Confirmation- only events (createToken, attribute changes, freeze/thaw, burn/destroy, accept-collection-transfer) get a generic "an event is emitted on FINALIZED confirming the action" line. Either way the page body stays clean — the Subscan workaround lives only on working-with-events and websocket-events, never repeated. Also two cleanups on stale event references in user-facing pages: section 6 of using-the-enjin-platform no longer lists "Listen to Platform Websocket Events" as a documented method (the feature isn't shipped), and the wallet-daemon page's "Wallet Daemon Events" section becomes "How the Wallet Daemon Works" — the diagram and the polling- based flow are still accurate, but the WebSocket subscribe/emit steps described an unshipped feature. Touched 5 Group A pages (existing event mentions cleaned up + linked), 9 managing-tokens guides, 1 managing-users guide, and 3 API-ref mutations pages. --- .../04-using-the-enjin-platform.md | 2 +- .../06-using-wallet-daemon.md | 21 ++++++++----------- .../01-creating-collections.md | 2 ++ .../010-transfer-accept-collection.md | 2 +- .../02-creating-a-currency-token.md | 2 ++ .../02-creating-tokens/02-creating-tokens.md | 2 ++ .../01-managing-tokens/03-adding-metadata.md | 2 ++ .../01-managing-tokens/04-minting-a-token.md | 2 ++ .../05-transferring-tokens.md | 2 ++ .../01-managing-tokens/07-freezing-thawing.md | 2 ++ .../08-burning-destroying-tokens.md | 2 ++ .../03-using-managed-wallets.md | 2 ++ .../01-queries/01-transactions-queries.md | 4 ++-- .../02-mutations/01-transaction-mutations.md | 2 ++ .../02-mutations/02-collections-mutations.md | 4 +++- .../02-mutations/03-tokens-mutations.md | 2 +- .../02-mutations/06-marketplace-mutations.md | 2 +- .../02-mutations/07-token-groups-mutations.md | 2 +- 18 files changed, 39 insertions(+), 20 deletions(-) diff --git a/docs/01-getting-started/04-using-the-enjin-platform.md b/docs/01-getting-started/04-using-the-enjin-platform.md index 5b5542f..b1ea2d8 100644 --- a/docs/01-getting-started/04-using-the-enjin-platform.md +++ b/docs/01-getting-started/04-using-the-enjin-platform.md @@ -285,7 +285,7 @@ To check the transaction status, head over to the [Transactions](https://platfor ![Transactions page showing a finalized create_collection transaction](/img/getting-started/v3-transactions-page.png) -Once the state reaches `FINALIZED`, click the row's **Extrinsic Hash** link to open the transaction on Subscan (the blockchain explorer). The **Events** tab there lists everything the transaction emitted on-chain — for a collection-creation transaction, that's where you'll find the newly minted **collection ID**. +Once the state reaches `FINALIZED`, the transaction has been included on-chain. The events it emitted — for example, the new **collection ID** after a collection-creation transaction — can be read via the flow described in [Working with Events](/05-enjin-platform/03-working-with-events.md). ### Receive Transaction Information Using the Enjin API & SDKs diff --git a/docs/01-getting-started/06-using-wallet-daemon.md b/docs/01-getting-started/06-using-wallet-daemon.md index 9e703d2..7fae52c 100644 --- a/docs/01-getting-started/06-using-wallet-daemon.md +++ b/docs/01-getting-started/06-using-wallet-daemon.md @@ -14,21 +14,18 @@ In the Enjin Platform context, the Wallet Daemon is a utility tool that manages The diagram above provides insight into the interaction between the Enjin Wallet Daemon and the Enjin Platform. This illustrates how the Wallet Daemon can communicate with the API in both directions, automatically signing and broadcasting transactions to the blockchain. This helps developers better understand how the two components work together seamlessly to provide a streamlined experience. -## Wallet Daemon Events +## How the Wallet Daemon Works ![A diagram of the wallet daemon](/img/getting-started/wallet-daemon-diagram.png) -The diagram above depicts the various stages involved in creating a new collection on the Enjin Matrixchain via the Enjin Platform API, with a particular emphasis on the Wallet Daemon events. - -1. **Establish a WebSocket connection -** Connect to a WebSocket server to subscribe to channels that receive events. -2. **Subscribe to a WebSocket channel -** Subscribe to the channel associated with your wallet to receive real-time events. -3. **Send a mutation -** Send a "CreateCollection" mutation to the Enjin Platform API to create a new collection on the Enjin Matrixchain. The API will encode the transaction that the wallet daemon needs to sign and save it in the Enjin Platform Database. -4. **Wallet daemon requests pending transactions -** The wallet daemon repeatedly asks the Enjin Platform API if there are any transactions to sign to prevent incoming connections to the wallet daemon, which holds your . -5. **Sign and broadcast the transaction -** After receiving the pending transaction, the wallet daemon signs it with your private key and broadcasts it to the Enjin Matrixchain. -6. **Enjin Matrixchain processes the transaction -** If everything is correct and valid, the Matrixchain successfully processes the extrinsic sent by the wallet daemon, and the new collection is created. -7. **Enjin Platform API monitors the chain -** The Enjin Platform API continually watches the chain via a WebSocket connection to detect any activity. -8. **Save the data to the database -** The Enjin Platform API stores the new collection data in its own database, allowing you to query it as desired. -9. **Emit an event -** The Enjin Platform API sends an event to the WebSocket channels, providing you with the opportunity to take appropriate actions if you're subscribed to the relevant channel. +The diagram above depicts the various stages involved in creating a new collection on the Enjin Matrixchain via the Enjin Platform API. + +1. **Send a mutation -** Your application calls `CreateTransaction` (with the `createCollection` discriminator) on the Enjin Platform API. The API encodes the corresponding extrinsic, stores it in its database with a `PENDING` state, and returns a transaction `uuid`. +2. **Wallet daemon requests pending transactions -** The wallet daemon repeatedly polls the Enjin Platform API for transactions to sign. This polling model — rather than the daemon accepting incoming connections — means the machine holding your never needs to expose an open port. +3. **Sign and broadcast the transaction -** After receiving the pending transaction, the wallet daemon signs it with your private key and broadcasts it to the Enjin Matrixchain. +4. **Enjin Matrixchain processes the transaction -** If everything is valid, the Matrixchain processes the extrinsic, and the new collection is created. +5. **Enjin Platform API monitors the chain -** The Enjin Platform API watches the chain to detect when your transaction is finalized and updates the transaction record accordingly. +6. **Read the result -** Poll [`GetTransaction(uuid:)`](/03-api-reference/01-queries/01-transactions-queries.md#gettransaction) for the final `state` and `extrinsicHash`. To read the on-chain events the transaction emitted (the new `collection_id`, for example), see [Working with Events](/05-enjin-platform/03-working-with-events.md). *** diff --git a/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md b/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md index c2964c3..3892f74 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md +++ b/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md @@ -269,6 +269,8 @@ print(response.json()) The response includes the transaction's `uuid`, `action` (e.g. `MultiTokens.create_collection`), and `state` (`PENDING` → `BROADCAST` → `FINALIZED`). Use `GetTransaction(network, chain, uuid: "")` to poll the current state. +Once it reaches `FINALIZED`, a `MultiTokens.CollectionCreated` event is emitted containing the new `collection_id` assigned to your collection. See [Working with Events](/05-enjin-platform/03-working-with-events.md) for how to read it. + :::info Explore More Arguments For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. diff --git a/docs/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md b/docs/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md index 5e0cec5..d6d6e18 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md +++ b/docs/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md @@ -373,4 +373,4 @@ print(response.json()) -Once the transaction is confirmed, the collection ownership will be transferred to the new owner. +Once the transaction is confirmed, the collection ownership will be transferred to the new owner. An event is emitted on `FINALIZED` confirming the change — see [Working with Events](/05-enjin-platform/03-working-with-events.md) for how to read it. diff --git a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md index 398bb4b..973de57 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md +++ b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md @@ -314,6 +314,8 @@ print(response.json()) The response includes the transaction's `uuid`, `action` (e.g. `MultiTokens.create_token`), and `state` (`PENDING` → `BROADCAST` → `FINALIZED`). Use `GetTransaction(network, chain, uuid: "")` to poll the current state. +Once it reaches `FINALIZED`, an event is emitted confirming the new currency token was created. See [Working with Events](/05-enjin-platform/03-working-with-events.md) for how to read it. + :::tip For Token ID management, head to [Best Practices > TokenID Structure](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md) ::: diff --git a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md index 67e96d1..55f5705 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md @@ -365,6 +365,8 @@ print(response.json()) The response includes the transaction's `uuid`, `action` (e.g. `MultiTokens.create_token`), and `state` (`PENDING` → `BROADCAST` → `FINALIZED`). Use `GetTransaction(network, chain, uuid: "")` to poll the current state. +Once it reaches `FINALIZED`, an event is emitted confirming the new token was created. See [Working with Events](/05-enjin-platform/03-working-with-events.md) for how to read it. + :::tip For Token ID management, head to [Best Practices > TokenID Structure](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md) ::: diff --git a/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md b/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md index 396e496..97e0fd7 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md +++ b/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md @@ -405,6 +405,8 @@ print(response.json()) The response includes the transaction's `uuid`, `action` (e.g. `MultiTokens.batch_set_attribute`), and `state` (`PENDING` → `BROADCAST` → `FINALIZED`). Use `GetTransaction(network, chain, uuid: "")` to poll the current state. +Once it reaches `FINALIZED`, an event is emitted for each attribute set, confirming the change. See [Working with Events](/05-enjin-platform/03-working-with-events.md) for how to read them. + :::tip Setting a single attribute, or removing one Use `setTokenAttribute` / `setCollectionAttribute` to write a single key, `removeTokenAttribute` / `removeCollectionAttribute` to remove one, and `removeAllTokenAttributes` / `removeAllCollectionAttributes` to wipe everything. All of these are discriminator fields on `CreateTransaction.transaction`. ::: diff --git a/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md b/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md index 4458bec..e6e9f93 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md +++ b/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md @@ -334,6 +334,8 @@ print(response.json()) The response includes the transaction's `uuid`, `action` (e.g. `MultiTokens.batch_mint`), and `state` (`PENDING` → `BROADCAST` → `FINALIZED`). Use `GetTransaction(network, chain, uuid: "")` to poll the current state. +Once it reaches `FINALIZED`, a `MultiTokens.Minted` event is emitted for each recipient with the minted amount — useful when you need to confirm a mint reached a specific player (e.g. before unlocking the corresponding in-game item). See [Working with Events](/05-enjin-platform/03-working-with-events.md) for how to read it. + :::info Explore More Arguments For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. diff --git a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md index 449f875..7f50d3b 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md @@ -694,6 +694,8 @@ This can be done using Enjin Platform API & WalletConnect! To learn more, check out the [Using WalletConnect page](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md). ::: +Once a transfer transaction reaches `FINALIZED`, a `MultiTokens.Transferred` event (for token transfers) or `Balances.Transfer` event (for ENJ transfers) is emitted with the sender, recipient, and amount — useful for reacting to inbound transfers in real time (e.g. unlocking an in-game item the moment a player receives the corresponding NFT). See [Working with Events](/05-enjin-platform/03-working-with-events.md) for how to read it. + :::info Explore More Arguments For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. diff --git a/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md b/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md index de11602..dd777c4 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md +++ b/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md @@ -938,6 +938,8 @@ print(response.json()) Once the transaction is executed, the specified token will be thawed. +For each of these actions, an event is emitted once the transaction reaches `FINALIZED` — useful as a confirmation signal. See [Working with Events](/05-enjin-platform/03-working-with-events.md) for how to read it. + :::info Explore More Arguments For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. diff --git a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md index eb87a69..443bca6 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md @@ -616,3 +616,5 @@ print(response.json()) Once the transaction is executed, the collection will be destroyed and the will be retrieved. + +For each of the burn / destroy actions on this page, an event is emitted once the transaction reaches `FINALIZED` — useful as a confirmation signal. See [Working with Events](/05-enjin-platform/03-working-with-events.md) for how to read it. diff --git a/docs/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md b/docs/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md index 2e7da17..bdc4e83 100644 --- a/docs/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md +++ b/docs/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md @@ -934,6 +934,8 @@ print(response.json()) Make sure `signerExternalId` (or `signerAccount`) is set to the managed wallet that holds the token being transferred. +Whether you're minting into a managed wallet or transferring out of one, the on-chain transaction emits the usual events on `FINALIZED` (e.g. `MultiTokens.Minted`, `MultiTokens.Transferred`) — with the managed wallet's address as the signer. See [Working with Events](/05-enjin-platform/03-working-with-events.md) for how to read them. + :::info Explore More Arguments For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. ::: \ No newline at end of file diff --git a/docs/03-api-reference/01-queries/01-transactions-queries.md b/docs/03-api-reference/01-queries/01-transactions-queries.md index d82e70a..23566f6 100644 --- a/docs/03-api-reference/01-queries/01-transactions-queries.md +++ b/docs/03-api-reference/01-queries/01-transactions-queries.md @@ -159,7 +159,7 @@ query GetBlock { :::warning GetBlock is currently unavailable -`GetBlock` is temporarily disabled — every call returns an `Internal server error`. For event-level lookups use the [Transactions page on the platform dashboard](https://platform.beta.enjin.io/transactions) and click the **Extrinsic Hash** link on a transaction's row, which opens its events on Subscan. The platform team is aware of the issue and a fix is planned. +`GetBlock` is temporarily disabled — every call returns an `Internal server error`. The platform team is aware of the issue and a fix is planned. For event-level lookups in the meantime, see [Working with Events](/05-enjin-platform/03-working-with-events.md). ::: ## GetBlocks @@ -205,7 +205,7 @@ query GetBlocks { :::warning Block.extrinsics and Block.events return null -The `extrinsics` and `events` sub-fields on `Block` are defined in the schema but currently return `null` on every request. For now, use the [Transactions dashboard](https://platform.beta.enjin.io/transactions) → **Extrinsic Hash** → Subscan flow for event-level data. +The `extrinsics` and `events` sub-fields on `Block` are defined in the schema but currently return `null` on every request. For event-level lookups in the meantime, see [Working with Events](/05-enjin-platform/03-working-with-events.md). ::: ## GetExtrinsic diff --git a/docs/03-api-reference/02-mutations/01-transaction-mutations.md b/docs/03-api-reference/02-mutations/01-transaction-mutations.md index c8b791d..dcdda39 100644 --- a/docs/03-api-reference/02-mutations/01-transaction-mutations.md +++ b/docs/03-api-reference/02-mutations/01-transaction-mutations.md @@ -13,6 +13,8 @@ import TabItem from '@theme/TabItem'; The Enjin Platform exposes a small number of root-level mutations. Almost every on-chain action — creating a collection, minting, transferring, listing, burning, freezing, bonding to a nomination pool, managing token groups — is funnelled through the single `CreateTransaction` mutation, with the action selected by which field is set on the `transaction` input. +Every action emits one or more on-chain events when the transaction reaches `FINALIZED` — see [Working with Events](/05-enjin-platform/03-working-with-events.md) for how to read them. + ## CreateTransaction Submits a single on-chain action. The `transaction` argument is a `TransactionInput` — set exactly one field on it to choose the action (`createCollection`, `createToken`, `mintToken`, `transferToken`, `transferEnj`, `burnToken`, `freezeToken`, `createListing`, and ~40 more). See [Important Arguments](/03-api-reference/04-important-arguments.md) for the per-field shapes. diff --git a/docs/03-api-reference/02-mutations/02-collections-mutations.md b/docs/03-api-reference/02-mutations/02-collections-mutations.md index bb98124..929e597 100644 --- a/docs/03-api-reference/02-mutations/02-collections-mutations.md +++ b/docs/03-api-reference/02-mutations/02-collections-mutations.md @@ -13,12 +13,14 @@ import TabItem from '@theme/TabItem'; All collection-level actions are submitted through [`CreateTransaction`](/03-api-reference/02-mutations/01-transaction-mutations.md#createtransaction), with the action selected by the field set on the `transaction` input. Each section below shows the discriminator and its expected fields. -The response shape is always a `Transaction` — the examples below all return the standard `{ uuid, action, state }` selection. +The response shape is always a `Transaction` — the examples below all return the standard `{ uuid, action, state }` selection. Once a transaction reaches `FINALIZED`, the on-chain event(s) it emitted can be read via the flow described in [Working with Events](/05-enjin-platform/03-working-with-events.md). ## createCollection Creates a new on-chain collection. Most fields are optional — the example below shows everything; a minimal call is just `transaction: { createCollection: {} }`. +The new collection's `id` is assigned by the chain — once the transaction finalizes, a `MultiTokens.CollectionCreated` event is emitted containing it. + ```graphql diff --git a/docs/03-api-reference/02-mutations/03-tokens-mutations.md b/docs/03-api-reference/02-mutations/03-tokens-mutations.md index 6061a89..9ed38aa 100644 --- a/docs/03-api-reference/02-mutations/03-tokens-mutations.md +++ b/docs/03-api-reference/02-mutations/03-tokens-mutations.md @@ -11,7 +11,7 @@ import TabItem from '@theme/TabItem'; `https://platform.beta.enjin.io/graphql` ::: -All token-level actions are submitted through [`CreateTransaction`](/03-api-reference/02-mutations/01-transaction-mutations.md#createtransaction), with the action selected by the field set on the `transaction` input. The response shape is always a `Transaction` — the examples below all return the standard `{ uuid, action, state }` selection. +All token-level actions are submitted through [`CreateTransaction`](/03-api-reference/02-mutations/01-transaction-mutations.md#createtransaction), with the action selected by the field set on the `transaction` input. The response shape is always a `Transaction` — the examples below all return the standard `{ uuid, action, state }` selection. Once a transaction reaches `FINALIZED`, the on-chain event(s) it emitted (e.g. `MultiTokens.Minted`, `MultiTokens.Transferred`, `MultiTokens.Burned`) can be read via the flow described in [Working with Events](/05-enjin-platform/03-working-with-events.md). For collection-level actions (creating, mutating, freezing the whole collection, attributes), see [Collections Mutations](/03-api-reference/02-mutations/02-collections-mutations.md). diff --git a/docs/03-api-reference/02-mutations/06-marketplace-mutations.md b/docs/03-api-reference/02-mutations/06-marketplace-mutations.md index e1b6fea..a0aebd5 100644 --- a/docs/03-api-reference/02-mutations/06-marketplace-mutations.md +++ b/docs/03-api-reference/02-mutations/06-marketplace-mutations.md @@ -120,7 +120,7 @@ mutation CreateOfferListing { -The listing's id is the hex string returned by the chain once the transaction finalizes — read it from `Marketplace.ListingCreated` events on the extrinsic, or by listing the collection via [`GetListings`](/03-api-reference/01-queries/06-marketplace-queries.md#getlistings). +Once the transaction finalizes, a `Marketplace.ListingCreated` event is emitted containing the new listing's id (a hex string assigned by the chain). See [Working with Events](/05-enjin-platform/03-working-with-events.md) for how to read it, or look the listing up after the fact via [`GetListings`](/03-api-reference/01-queries/06-marketplace-queries.md#getlistings). ## fillListing diff --git a/docs/03-api-reference/02-mutations/07-token-groups-mutations.md b/docs/03-api-reference/02-mutations/07-token-groups-mutations.md index 92b0619..273437b 100644 --- a/docs/03-api-reference/02-mutations/07-token-groups-mutations.md +++ b/docs/03-api-reference/02-mutations/07-token-groups-mutations.md @@ -15,7 +15,7 @@ A token group is an on-chain bucket of tokens within a single collection — see ## createTokenGroup -Creates an empty token group inside a collection. The group's `id` is assigned by the chain; read it back via [`GetCollection.tokenGroups`](/03-api-reference/01-queries/07-token-groups-queries.md#reading-all-groups-in-a-collection) once the transaction finalizes (or by inspecting the `MultiTokens.TokenGroupCreated` event on Subscan). +Creates an empty token group inside a collection. Once the transaction finalizes, a `MultiTokens.TokenGroupCreated` event is emitted containing the new group's `id` (assigned by the chain) — see [Working with Events](/05-enjin-platform/03-working-with-events.md) for how to read it. You can also list the collection's groups after the fact via [`GetCollection.tokenGroups`](/03-api-reference/01-queries/07-token-groups-queries.md#reading-all-groups-in-a-collection). From 541e9ecdfa4e52486923e8f6a0f729a6178f9973 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Tue, 26 May 2026 14:36:49 +0300 Subject: [PATCH 47/67] Document built-in Canary faucet and sweep sunset faucet links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Get Test Funds section to using-the-enjin-platform.md describing the built-in Canary faucet (switch to Canary, click Faucet icon, 250 cENJ to the Wallet Daemon's wallet). Renumber subsequent sections; all inbound links target explicit {#finding-tokens} / {#batching-transactions} anchors so renumbering doesn't break them. Replace every link to the sunset faucet.canary.enjin.io across 16 pages with a forward-pointer to the new {#canary-faucet} section. Faucet icon screenshot is a placeholder PNG — to be replaced. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../04-using-the-enjin-platform.md | 28 +++++++++++++++--- .../05-using-enjin-api/05-using-enjin-api.md | 2 +- .../01-creating-collections.md | 2 +- .../02-creating-a-currency-token.md | 2 +- .../02-creating-tokens/02-creating-tokens.md | 2 +- .../01-managing-tokens/03-adding-metadata.md | 2 +- .../01-managing-tokens/04-minting-a-token.md | 2 +- .../05-transferring-tokens.md | 2 +- .../01-managing-tokens/06-create-qr-drops.md | 2 +- .../01-managing-tokens/07-freezing-thawing.md | 2 +- .../08-burning-destroying-tokens.md | 2 +- .../01-using-wallet-connect.md | 2 +- .../03-using-managed-wallets.md | 2 +- .../02-managing-users/04-using-fuel-tanks.md | 2 +- .../01-setup-guide.md | 4 +-- docs/06-enjin-products/02-enjin-coin.md | 2 +- .../getting-started/v3-canary-faucet-icon.png | Bin 0 -> 70 bytes 17 files changed, 40 insertions(+), 20 deletions(-) create mode 100644 static/img/getting-started/v3-canary-faucet-icon.png diff --git a/docs/01-getting-started/04-using-the-enjin-platform.md b/docs/01-getting-started/04-using-the-enjin-platform.md index b1ea2d8..a5aaf40 100644 --- a/docs/01-getting-started/04-using-the-enjin-platform.md +++ b/docs/01-getting-started/04-using-the-enjin-platform.md @@ -10,7 +10,7 @@ import TabItem from '@theme/TabItem'; :::info What you'll need: Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) to pay for and for s. -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +You can obtain cENJ (Canary ENJ) for testing from the [built-in Canary faucet](#canary-faucet) in the Platform UI. ::: ## 1. Set Up an Account @@ -45,7 +45,27 @@ Once the daemon is running and connected, your Platform account is ready to use. On most transaction creation forms in the Platform UI, you can override the default signer using the **Transaction Options → Signing Account** field. Setting it to a Managed Wallet's address signs the transaction with that wallet's stored key instead of with the Wallet Daemon's wallet. See [Using Managed Wallets](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) for details. ::: -## 4. Create Platform Requests +## 4. Get Test Funds (Canary Faucet) {#canary-faucet} + +To create or transact on the Canary testnet, your signing wallet needs some cENJ (Canary ENJ) to cover transaction fees and storage deposits. The Enjin Platform has a built-in faucet that drops cENJ directly to the Wallet Daemon's wallet. + +**Prerequisites:** A linked Wallet Daemon — if you haven't set one up yet, see [Set Up the Wallet Daemon](#3-set-up-the-wallet-daemon) above. + +**Steps:** + +1. In the **network selector** at the top-right corner of the dashboard, switch to **Canary**. +2. A **Faucet icon** appears next to the selector. Click it. +3. The faucet sends **250 cENJ** to your Wallet Daemon's wallet. + +![Network selector switched to Canary, with the Faucet icon next to it](/img/getting-started/v3-canary-faucet-icon.png) + +That's it — your Wallet Daemon's wallet now has enough cENJ to cover fees and deposits while you build and test on Canary. + +:::info Funding a different wallet +The faucet only sends to the Wallet Daemon's wallet. If you need cENJ in a [Managed Wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) or an external wallet, drop the faucet funds first, then transfer them from the daemon to your target wallet. +::: + +## 5. Create Platform Requests Now that the Platform account is set up, we can start creating platform requests. In this example we will be creating a . @@ -238,7 +258,7 @@ The response includes a `uuid` you can use to query the transaction's status as Once a request is created — whether through the dashboard or the API — the Wallet Daemon (or the [Managed Wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) you specified in the `Signing Account` field) picks it up, signs it, and broadcasts it to the chain. There is no manual approval step. The transaction will move from `PENDING` → `BROADCAST` → `FINALIZED` as it's processed. ::: -## 5. Working with the Dashboard +## 6. Working with the Dashboard A couple of dashboard patterns are worth knowing before you start managing existing collections and tokens. @@ -270,7 +290,7 @@ A successful submit produces a single transaction `uuid` on the [Transactions](h The API equivalent of the Batch Queue is the `CreateBatchTransaction` mutation — it takes a `transactions: [TransactionInput!]!` list where each entry is one of the same discriminator inputs you'd pass to `CreateTransaction`. ::: -## 6. Receive Transaction Information +## 7. Receive Transaction Information There are two ways to receive the transaction status and information: diff --git a/docs/01-getting-started/05-using-enjin-api/05-using-enjin-api.md b/docs/01-getting-started/05-using-enjin-api/05-using-enjin-api.md index 9ca124b..c8e7f6d 100644 --- a/docs/01-getting-started/05-using-enjin-api/05-using-enjin-api.md +++ b/docs/01-getting-started/05-using-enjin-api/05-using-enjin-api.md @@ -12,7 +12,7 @@ The Enjin API is a set of programmatic interfaces that allow developers to inter :::info What you'll need: - Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) to pay for . -- You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +- You can obtain cENJ (Canary ENJ) for testing from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. - An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). ::: diff --git a/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md b/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md index 3892f74..e7e6774 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md +++ b/docs/02-guides/01-platform/01-managing-tokens/01-creating-collections.md @@ -10,7 +10,7 @@ import TabItem from '@theme/TabItem'; :::info What you'll need: - Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for and 6.25 ENJ for s. -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +You can obtain cENJ (Canary ENJ) for testing from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. - An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). ::: diff --git a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md index 973de57..66aef4b 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md +++ b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md @@ -11,7 +11,7 @@ Similarly to [Tokens](/02-guides/01-platform/01-managing-tokens/02-creating-toke :::info What you'll need: - Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to process transactions and at least 0.01 ENJ for the . -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +You can obtain cENJ (Canary ENJ) for testing from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. - An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). - A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) to place the tokens in. ::: diff --git a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md index 55f5705..fb67413 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md @@ -12,7 +12,7 @@ import TabItem from '@theme/TabItem'; :::info What you'll need: - Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to process transactions and at least 0.01 ENJ for the . -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +You can obtain cENJ (Canary ENJ) for testing from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. - An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). - A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) to place the tokens in. ::: diff --git a/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md b/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md index 97e0fd7..71bf234 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md +++ b/docs/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md @@ -12,7 +12,7 @@ Adding to a token enables games and apps to impor :::info What you'll need: - Some [ Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for and metadata s. -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +You can obtain cENJ (Canary ENJ) for testing from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. - An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). - A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to add metadata to. - Follow our [Best Practices for Collection/Token Metadata](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md). diff --git a/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md b/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md index e6e9f93..3a53e28 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md +++ b/docs/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md @@ -13,7 +13,7 @@ Now that you've got your tokens created, it's time to start minting them into pl :::info What you'll need: - Some [ Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for and a deposit of 0.01 ENJ is required for the , for each new token holder. If the token has , each new unit minted will require the same amount of ENJ to be infused. -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +You can obtain cENJ (Canary ENJ) for testing from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. - An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). - A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to mint. ::: diff --git a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md index 7f50d3b..2330781 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md @@ -16,7 +16,7 @@ You will need to transfer tokens for: :::info What you'll need: - Some [ Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for and a deposit of 0.01 ENJ is required for the , for each new token holder. -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +You can obtain cENJ (Canary ENJ) for testing from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. - An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). - A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to mint. ::: diff --git a/docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md b/docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md index 55bf39c..79480f6 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md +++ b/docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md @@ -12,7 +12,7 @@ import TabItem from '@theme/TabItem'; :::info What you'll need: - Some [ Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for and a deposit of 0.01 ENJ is required for the , for each new token holder. -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +You can obtain cENJ (Canary ENJ) for testing from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. - An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). - A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to mint. ::: diff --git a/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md b/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md index dd777c4..637e5aa 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md +++ b/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md @@ -34,7 +34,7 @@ A **freeze state** determines whether a token can be transferred and the conditi :::info What you'll need: - Some [ Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for . -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +You can obtain cENJ (Canary ENJ) for testing from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. - An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). - A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to freeze. ::: diff --git a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md index 443bca6..10567ec 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md @@ -13,7 +13,7 @@ Melting a token with releases the Infused ENJ :::info What you'll need: - Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for . -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +You can obtain cENJ (Canary ENJ) for testing from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. - An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). - A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) or a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to melt/destroy. ::: diff --git a/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md b/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md index 39e6f22..79f9d80 100644 --- a/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md +++ b/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md @@ -23,7 +23,7 @@ The `polkadot-onboard` sample repository used in this guide is **no longer maint - Node.js 18+ and yarn installed - A mobile device with [Enjin Wallet app](https://enjin.io/wallet) installed - A Canary wallet with at least 6.5 ENJ in it to cover for and . -You can obtain cENJ for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +You can obtain cENJ for testing from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. - An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md) (Canary). ::: diff --git a/docs/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md b/docs/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md index bdc4e83..db0f1ee 100644 --- a/docs/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md +++ b/docs/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md @@ -23,7 +23,7 @@ This process ensures that users can interact with blockchain assets seamlessly w :::info What you'll need: - Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for . -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +You can obtain cENJ (Canary ENJ) for testing from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. - An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). - A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to add to the wallet. ::: diff --git a/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md b/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md index be3c762..7acc5d6 100644 --- a/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md +++ b/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md @@ -21,7 +21,7 @@ When dispatching a transaction via a Fuel Tank, the following checks must pass: :::info What you'll need: - Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin / Canary Matrixchain to pay for , and for funding the tank. -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). +You can obtain cENJ (Canary ENJ) for testing from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. - An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). ::: diff --git a/docs/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md b/docs/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md index 0e57249..f2c88a2 100644 --- a/docs/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md +++ b/docs/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md @@ -30,7 +30,7 @@ Before you begin, make sure you have the following installed: - ✅ **Node.js** (which includes `npm`). - ✅ **Git** for cloning the repositories. - ✅ An **Enjin Platform account**. If you don't have one, you can create it [here](https://platform.beta.enjin.io/). - - ✅ Some cENJ tokens (can be aquired from the [cENJ Faucet](https://faucet.canary.enjin.io/)) + - ✅ Some cENJ tokens (can be acquired from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI) ----- @@ -169,7 +169,7 @@ You're all set up and ready to play.

:::warning cENJ funds are required New managed wallet have no funds. To melt or transfer tokens out of a managed wallet, you'll need to fund it with some cENJ, or set up a fuel tank. -To receive cENJ funds for testing, use the [cENJ faucet](https://faucet.canary.enjin.io/) +To receive cENJ funds for testing, use the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. ::: :::info Understanding the code diff --git a/docs/06-enjin-products/02-enjin-coin.md b/docs/06-enjin-products/02-enjin-coin.md index f48fe73..550e5ec 100644 --- a/docs/06-enjin-products/02-enjin-coin.md +++ b/docs/06-enjin-products/02-enjin-coin.md @@ -12,7 +12,7 @@ It is used to pay  (also known as can come in three different forms: - **:** This is the native version of Enjin Coin. You can buy it on cryptocurrency exchanges and use it for each or use it to create new s on the Enjin Blockchain . -- **cENJ:** This is the version of Enjin Coin. It's available for free from the [cENJ faucet](https://faucet.canary.enjin.io/) and can be used just like ENJ for transactions and minting s, but on the Enjin Canary Testnet. +- **cENJ:** This is the version of Enjin Coin. It's available for free from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Enjin Platform UI and can be used just like ENJ for transactions and minting s, but on the Enjin Canary Testnet. - **sENJ:** sENJ represents staked ENJ. It's issued to you when you [stake your ENJ](/02-guides/02-blockchain/01-relaychain/04-joining-nomination-pools.md) and is destroyed when you unstake. You can your ENJ into a or . Once you've received your sENJ from staking, it can be [traded for ENJ](/04-enjin-blockchain/02-enjin-relaychain/02-stake-exchange-pallet.md). ## ENJ Infusion diff --git a/static/img/getting-started/v3-canary-faucet-icon.png b/static/img/getting-started/v3-canary-faucet-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..08cd6f2bfd1b53ec5a4db72bed55f40907e8bdfa GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k92}1TpU9xZY8JuI3K{zz}&{z5M@%E Q4U}N;boFyt=akR{0J Date: Tue, 26 May 2026 14:39:56 +0300 Subject: [PATCH 48/67] Rewrite the Quick Start guide for v3 Replace v2 wallet-connect / SimpleTransferToken / GetWallet flows with the current Platform behavior: - Section 2: Connect Your Wallet -> Set Up the Wallet Daemon for Signing Transactions. v3 has no manual wallet-connect step; signing is handled by the daemon (or optionally a Managed Wallet per tx). Drop the connect-wallet gif and the now-redundant Daemon bullet from Section 7. - Section 3: Fix the broken Tokens-page link. Token creation goes Collections -> click collection -> Create Token. Drop both v2 images. Note transactions sign automatically via the daemon. - Section 4: Drop the RequestAccount / GetAccountVerified GraphQL impl (both removed). Keep the conceptual intro + add a callout that the native wallet linking flow is in development. - Section 5: Replace SimpleTransferToken with CreateTransaction + transferToken discriminator, flat tokenId, uuid/action response. Add events forward-pointer. - Section 6: Replace GetWallet + tokenAccounts.edges.node with v3 GetAccount + flat tokens list. Note per-token amount support is being added. - Intro callout: refresh bullets to match the new flow. - Forward-pointer to the new Canary faucet section from Section 2. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../02-quick-start-guide.md | 213 +++++++----------- 1 file changed, 82 insertions(+), 131 deletions(-) diff --git a/docs/01-getting-started/02-quick-start-guide.md b/docs/01-getting-started/02-quick-start-guide.md index 1091593..c863180 100644 --- a/docs/01-getting-started/02-quick-start-guide.md +++ b/docs/01-getting-started/02-quick-start-guide.md @@ -13,11 +13,11 @@ Welcome! This guide provides the essential steps to quickly integrate the . -- Connected your developer wallet to authorize actions. +- Set up the to sign your transactions automatically. +- Funded your daemon's wallet with cENJ from the built-in Canary faucet. - Created your first blockchain and . -- Learned how to link your users' s to your application. - - Sent a token to a linked user wallet. - - Read tokens from a linked user wallet +- Sent a token to a wallet address. +- Read the tokens held by a wallet address. ::: ## 1. Create Your Enjin Platform Account @@ -34,22 +34,25 @@ The Enjin Platform is your central hub for managing blockchain projects, collect 4. Verify your email address by clicking the link sent to your inbox. 5. Log in to your newly created Enjin Platform account. -## 2. Connect Your Wallet Account +## 2. Set Up the Wallet Daemon for Signing Transactions -### Why Connect a Wallet? +### Why a Wallet Daemon? -To perform actions on the blockchain (like creating collections or minting tokens) through the Platform, you need to authorize these actions. Connecting your developer wallet (like the Enjin Wallet app) allows the Platform to request your signature for transactions you initiate. +Every on-chain action you initiate through the Platform — creating a collection, minting a token, transferring — needs to be signed by an account that holds funds. The **Enjin ** is a small service that runs alongside the Platform and signs your transactions automatically as you submit them, so you don't have to approve each one manually. -### Steps to Connect Your Wallet: +### Steps to Set Up: -To connect your wallet, click on the "Connect Wallet" button in the top right corner and follow the on-screen instructions. +1. On the [Account Settings](https://platform.beta.enjin.io/settings) page, click **Create API Token** and copy the new token somewhere safe. +2. Follow the [Wallet Daemon Setup guide](/01-getting-started/06-using-wallet-daemon.md) to download, configure, and run the daemon. It will use the API token from step 1 to know which Platform account to sign for. -![Connecting a wallet app on the Enjin Platform](/img/getting-started/connect-wallet-app.gif) +Once the daemon is running and connected, every transaction you submit from this Platform account is signed automatically. -:::tip Automating Signatures with the -For applications requiring automated actions (like minting rewards in real-time or batch transferring items without manual confirmation each time), you can set up the **Enjin Wallet Daemon**. This secure service runs on your server and can automatically sign transaction requests sent to your Enjin Platform. +:::tip Get cENJ for testing +To create or transact on Canary, your daemon's wallet needs some cENJ to cover fees and deposits. See [Get Test Funds (Canary Faucet)](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) — one click in the dashboard drops 250 cENJ into your daemon's wallet. +::: -**Learn More:** [Set up the Enjin Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) +:::info Signing with a Managed Wallet +You can override the default signer on a per-transaction basis by setting a [Managed Wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) address in the **Transaction Options → Signing Account** field of any transaction form. ::: ## 3. Create Your First Collection and Token @@ -81,21 +84,17 @@ You can interact with the Enjin Platform and initiate blockchain requests in sev ::: 1. **Create Collection:** - - In the Platform menu, navigate to the **[Collections page](https://platform.beta.enjin.io/collections)**. Then, click on the "**[Create Collection](https://platform.beta.enjin.io/create/collection)**" button. - ![Creating a collection](/img/guides/managing-tokens/create-collection.gif) - - Customize your collection - you can specify the collection's name, description, media, attributes, Mint and Market policies, and more. - To learn more about collection configuration, please check the [Create Collections guide](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md). - - Submit the creation request by clicking on the **Create** button. This will create a signature request which you can view in the [Transactions page](https://platform.beta.enjin.io/transactions), and will require a transaction to be signed. - ![](/img/guides/managing-tokens/pending-create-collection-txn.png) - - To sign the Create Collection transaction request - Click on the **"Sign"** button and approve the signature request with your connected wallet. - - Once the transaction is confirmed on-chain, the new collection will appear in your [Collections page](https://platform.beta.enjin.io/collections), where you can also find its ID, keep note of it for the next step. + - In the Platform menu, navigate to **[Collections](https://platform.beta.enjin.io/collections)** and click the "**Create Collection**" button. + - Customize your collection — you can specify its Mint Policy, Royalties, Explicit Royalty Currencies, and Attributes. + To learn more about collection configuration, see the [Create Collections guide](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md). + - Click **Create** to submit. The Wallet Daemon signs and broadcasts the transaction automatically — watch it move from `PENDING` → `BROADCAST` → `FINALIZED` on the [Transactions page](https://platform.beta.enjin.io/transactions). + - Once finalized, the new collection appears on the [Collections page](https://platform.beta.enjin.io/collections) with its assigned `id`. Keep that id handy for the next step. 2. **Create Token:** - - In the Platform menu, navigate to the **[Tokens page](https://platform.beta.enjin.io/tokens)**. Then, click on the "**[Create Token](https://platform.beta.enjin.io/create/token)**" button. - - Customize your token - you can specify the token's name, description, media, attributes, and more. - To learn more about token configuration, please check the [Create Tokens guide](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md). - - In the **"Collection ID"** input field, insert the collection id of the collection you just created. You may also click the input field and select the id from the dropdown menu. - - In the **"Recipient"** input field, insert the of the wallet that you wish to receive the token's initial supply. You can find the address of your connected wallet in the [settings page](https://platform.beta.enjin.io/settings) by clicking on the Settings icon in the top right corner, followed by the "Settings" button. - - Submit the creation request by clicking on the **Create** button. This requires another transaction, which you can sign in the [Transactions page](https://platform.beta.enjin.io/transactions). + - Open the [Collections page](https://platform.beta.enjin.io/collections), click the collection you just created, then click the "**Create Token**" button at the top of the collection's page. + - Customize your token — Token ID, initial supply, cap, royalty behavior, and attributes. + To learn more about token configuration, see the [Create Tokens guide](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md). + - In the **Recipient** input field, paste the of the wallet that should receive the token's initial supply. To send the supply to your own daemon's wallet, copy its address from the [Settings page](https://platform.beta.enjin.io/settings). + - Click **Create** to submit. The Wallet Daemon signs and broadcasts the transaction automatically; track it on the [Transactions page](https://platform.beta.enjin.io/transactions). :::tip Programmatic Creation You can also create collections and tokens using the API or SDKs. @@ -113,11 +112,10 @@ To interact with a specific user's blockchain items (check their holdings, send The typical flow for this is: -1. Your application requests a unique wallet verification QR code from the Enjin API. -2. Your application displays this QR code to the user. -3. The user scans the QR code with their Enjin Wallet app. -4. The wallet confirms ownership, securely providing your application with the user's public wallet address. -5. Your application then saves this verified wallet address and **associates** it with the user's internal ID in your database. +1. Your application requests a unique wallet verification challenge from the Enjin API. +2. Your application surfaces the challenge to the user (for example, via a QR code or a deep link). +3. The user approves the request in their wallet, securely providing your application with their public wallet address. +4. Your application then saves this verified wallet address and **associates** it with the user's internal ID in your database. Once this association (or "link") is established, you can: @@ -125,153 +123,108 @@ Once this association (or "link") is established, you can: - Airdrop or transfer items directly to the user's associated wallet. - Display their relevant blockchain inventory within your app. -### Implementation (GraphQL Example): - -To request a wallet verification code, we need to interact with the **** -The Enjin Platform API is a API. If you are unfamiliar with GraphQL, we recommend checking out the [How to Use GraphQL](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md) guide. - -:::info Quick Start: Sending API Requests with the -In this example, we will be using the to easily send an API request from within the browser. -In a production-ready integration, your app will typically send API calls using HTTP requests or one of the [official Enjin SDKs](/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md). You can learn more about this in the [Using the Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) page. -::: - -To interact with the Enjin Platform API, head over to the [Enjin Platform](https://platform.beta.enjin.io/), click on the settings icon in the top right corner, followed by **"platform-core"** in the available packages. This will navigate to the [Enjin Platform GraphiQL Playground](https://platform.beta.enjin.io/graphiql/). - -Once you are in the GraphiQL playground page, copy & paste the following query: - -```graphql -query RequestAccount{ - RequestAccount{ - qrCode #Returns a QR code for your user to scan - verificationId #An id to this account request call. Save this for next step - } -} -``` - -This will return a link to a QR Code for you to display to your user, and a unique verification id to track the request. -In your application, you'll display this QR code to your user and instruct them to scan it with their . - -To confirm that the user scanned the QR code and approved the request, copy & paste the following query (replace the `replace_verification_id_here` placeholder with the `verificationId` from the previous call's response): - -```graphql -query GetAccountVerified{ - GetAccountVerified(verificationId: "replace_verification_id_here"){ - verified - account{ - publicKey - address - } - } -} -``` - -If the user successfully scanned the QR code and approved the request, the response will show `"verified": true` and contain the user's wallet address: +### Implementation -```json -{ - "data": { - "GetAccountVerified": { - "verified": true, - "account": { - "publicKey": "0x5a6aae294416f3e875d9a8975658905002cfd3e5e64105d76296c4b0adbfd77e", //The public key of the verified account - "address": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //The address of the verified account - } - } - } -} -``` +A native wallet linking flow is currently in development. Once it ships, this section will document the end-to-end implementation. In the meantime, you can have users share their wallet address directly (for example, by pasting it into a form) and use that address in the steps below. -At this point, your application should take the `address` value from the response and store it in your user database, associated with the specific user who scanned the QR code. This is the crucial step that "links" the user's in-app identity with their blockchain wallet address. - -:::tip Learn more about linking user wallets -Check the full in-depth guide: [Linking User Wallets](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md). +:::info Reference +The [Linking User Wallets](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md) page will become the full in-depth guide once the flow ships. ::: ## 5. Send a Token to a User ### Why Send Tokens? -This is a core interaction – transferring an NFT from your project's wallet (or another source) to a user's linked wallet. This could be for: +This is a core interaction — transferring an NFT from your project's wallet (or another source) to a user's wallet. Common scenarios include: - Rewarding achievements. - In-app purchases. - Airdrops or giveaways. ### Implementation (GraphQL Example): -Sending tokens involves creating a transfer request via the API. You'll need the Token ID, the recipient's linked wallet address, and the amount (for s). +Sending a token uses the same `CreateTransaction` mutation as the rest of the Platform, with the `transferToken` field set on the `transaction` input. You'll need the recipient's address, the collection id, the token id, and the amount. -Copy & paste the following mutation into the GraphiQL playground: +Run the following from the [Enjin Platform GraphiQL Playground](https://platform.beta.enjin.io/graphiql) — or from your own GraphQL client (in which case add `Authorization: Bearer ` to your request headers): ```graphql -mutation TransferToken{ - SimpleTransferToken( - collectionId: 36105 #Specify the collection ID here - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the recipent address here - params: { - tokenId: {integer: 0} #Specify the token ID - amount: 1 #Choose the transfer amount +mutation TransferToken { + CreateTransaction( + network: CANARY + chain: MATRIX + transaction: { + transferToken: { + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" # any wallet address you control for testing + collectionId: 36105 + tokenId: 0 + amount: 1 + } } - ){ - id - method + ) { + uuid + action state } } ``` -:::note This request needs to be signed. -You can sign it in the [Transactions page](https://platform.beta.enjin.io/transactions), as we did in the previous steps. -::: +The Wallet Daemon signs and broadcasts automatically. The response includes the transaction's `uuid` — use it to poll status with `GetTransaction(network: CANARY, chain: MATRIX, uuid: "")` or watch the row on the [Transactions page](https://platform.beta.enjin.io/transactions). + +Once finalized, the transfer event is emitted with the recipient and amount — see [Working with Events](/05-enjin-platform/03-working-with-events.md) for how to read it. :::tip Learn more about sending tokens Check the full in-depth guide: [Sending Tokens](/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md). ::: -## 6. Reading Linked User's Tokens +## 6. Reading a User's Tokens ### Why Read User Tokens? -Once a user has linked their wallet (as shown in [Step 4](#4-link-a-users-wallet-to-your-application)), you'll often need to check which tokens they own within your collection(s). This allows you to: +Once you know a user's wallet address, you'll often need to check which tokens they hold. This lets you: -- Display their relevant inventory within your game or application interface (like in-game currency or an item). +- Display their relevant inventory in your game or app interface. - Verify ownership of specific NFTs to grant access, perks, or abilities. - Trigger game logic based on the items they hold. ### Implementation (GraphQL Example): -To read the user's wallet, copy & paste the following query into the GraphiQL playground, using the user's linked wallet address: +Use the `GetAccount` query to read an address's holdings. Run this from the [GraphiQL Playground](https://platform.beta.enjin.io/graphiql) or your own GraphQL client: ```graphql -query FetchingWalletTokens{ - GetWallet(account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"){ #Specify the account address - tokenAccounts{ - edges{ - node{ - balance - token{ - tokenId - collection{ - collectionId - } - attributes{ - key - value - } - } - } +query FetchingWalletTokens { + GetAccount( + network: CANARY + chain: MATRIX + address: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" + ) { + address + balance + tokens { + tokenId + collection { + id + } + attributes { + key + value } } } } ``` +`balance` is the address's total ENJ balance in base units — divide by 10^18 for ENJ. `tokens` lists every token the wallet currently holds across all collections. + +:::info Per-token amount support is being added +`GetAccount` currently tells you *which* tokens a wallet holds (and each token's metadata), but not *how many* units of each. Per-holder balance support is in development — this query will include amount information once it ships. +::: + :::tip Learn more about reading user wallet balances Check the full in-depth guide: [Reading User Wallet Balances](/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md). ::: ## 7. Next Steps and Advanced Features -Congratulations! You've covered the basics of setting up your project on the Enjin Platform, creating items, and enabling user wallet interactions. +Congratulations! You've covered the basics of setting up your project on the Enjin Platform, creating items, and reading and transferring tokens. :::warning **Ready for Production?** Remember, this guide used the **Canary testnet** for development and testing. When you are ready to launch your application live, you will need to: @@ -283,7 +236,5 @@ Now you can explore more advanced integrations: - **On-Demand Minting:** Programmatically create *new* tokens directly into users' wallets based on real-time application events (e.g., crafting an item, completing a quest). This avoids pre-minting large supplies. ➡️ [Learn about On-Demand Minting](/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md#option-b-using-the-enjin-api--sdks) -- **Enjin Wallet Daemon:** If your application requires frequent, automated minting or transferring without manual intervention, setting up the Wallet Daemon is highly recommended for secure signature automation. - ➡️ [Set up the Enjin Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) - **Managed Wallets:** Simplify onboarding for users unfamiliar with crypto wallets. You can create and manage wallets for your users within the platform. Users can interact with blockchain features seamlessly, and later choose to export their items to their own self-custody Enjin Wallet if they wish. This significantly lowers the barrier to entry for Web2 users. - ➡️ [Explore Managed Wallets](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) \ No newline at end of file + ➡️ [Explore Managed Wallets](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) From 562319fa69341477fee1b8377e4fe655f11558af Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Tue, 26 May 2026 14:47:55 +0300 Subject: [PATCH 49/67] Add real Canary faucet icon screenshot Replaces the 1x1 placeholder PNG with the actual screenshot of the network selector + faucet icon in the Platform UI. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../getting-started/v3-canary-faucet-icon.png | Bin 70 -> 11904 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/static/img/getting-started/v3-canary-faucet-icon.png b/static/img/getting-started/v3-canary-faucet-icon.png index 08cd6f2bfd1b53ec5a4db72bed55f40907e8bdfa..892762cd2860d9301675c8ec3e8302a357f2ac38 100644 GIT binary patch literal 11904 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 ob&5qqDM4Xzq90&^#QV0V0n Date: Tue, 26 May 2026 15:05:30 +0300 Subject: [PATCH 50/67] Drop Managed Wallet callout from quick-start, add collection-card screenshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Managed Wallet callout in Section 2 was redundant for a starter audience — Managed Wallets remain mentioned in the Next Steps section where they belong as an advanced feature. Add the new v3-collection-card.png screenshot showing the new collection with its assigned ID, illustrating step 1 of Section 3. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/01-getting-started/02-quick-start-guide.md | 5 +---- .../img/getting-started/v3-collection-card.png | Bin 0 -> 67929 bytes 2 files changed, 1 insertion(+), 4 deletions(-) create mode 100644 static/img/getting-started/v3-collection-card.png diff --git a/docs/01-getting-started/02-quick-start-guide.md b/docs/01-getting-started/02-quick-start-guide.md index c863180..6985d6a 100644 --- a/docs/01-getting-started/02-quick-start-guide.md +++ b/docs/01-getting-started/02-quick-start-guide.md @@ -51,10 +51,6 @@ Once the daemon is running and connected, every transaction you submit from this To create or transact on Canary, your daemon's wallet needs some cENJ to cover fees and deposits. See [Get Test Funds (Canary Faucet)](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) — one click in the dashboard drops 250 cENJ into your daemon's wallet. ::: -:::info Signing with a Managed Wallet -You can override the default signer on a per-transaction basis by setting a [Managed Wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) address in the **Transaction Options → Signing Account** field of any transaction form. -::: - ## 3. Create Your First Collection and Token ### What are Collections and Tokens? @@ -89,6 +85,7 @@ You can interact with the Enjin Platform and initiate blockchain requests in sev To learn more about collection configuration, see the [Create Collections guide](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md). - Click **Create** to submit. The Wallet Daemon signs and broadcasts the transaction automatically — watch it move from `PENDING` → `BROADCAST` → `FINALIZED` on the [Transactions page](https://platform.beta.enjin.io/transactions). - Once finalized, the new collection appears on the [Collections page](https://platform.beta.enjin.io/collections) with its assigned `id`. Keep that id handy for the next step. + ![A collection with the collection ID next to it](/img/getting-started/v3-collection-card.png) 2. **Create Token:** - Open the [Collections page](https://platform.beta.enjin.io/collections), click the collection you just created, then click the "**Create Token**" button at the top of the collection's page. - Customize your token — Token ID, initial supply, cap, royalty behavior, and attributes. diff --git a/static/img/getting-started/v3-collection-card.png b/static/img/getting-started/v3-collection-card.png new file mode 100644 index 0000000000000000000000000000000000000000..eb3adfb268632da1aa2b2fdae618dbe76d42663e GIT binary patch literal 67929 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 Date: Wed, 27 May 2026 16:09:11 +0300 Subject: [PATCH 51/67] Rewrite Verifying Wallets as Sending Wallet Requests The v2 QR-based wallet verification flow (RequestAccount / GetAccountVerified / GetWallet(verificationId:)) is gone in v3. A native wallet-request feature is in development that will let applications link a player's wallet once and then send transaction-signing prompts straight to their Enjin Wallet app. Refocus this page on that flow: drop all v2 code, explain the two-step model conceptually, and add four gameplay-oriented example use cases (consume a potion, list a sword on the marketplace, infuse ENJ, stake into a nomination pool) with the standard FINALIZED-event forward-pointer. A "Coming soon" callout flags that exact shapes will land once v3 exits beta. Renumbered so this page precedes Using WalletConnect, and patched the inbound link from the Quick Start guide. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../02-quick-start-guide.md | 2 +- .../01-sending-wallet-requests.md | 69 ++ ...-connect.md => 02-using-wallet-connect.md} | 0 .../02-verifying-wallets.md | 717 ------------------ 4 files changed, 70 insertions(+), 718 deletions(-) create mode 100644 docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-sending-wallet-requests.md rename docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/{01-using-wallet-connect.md => 02-using-wallet-connect.md} (100%) delete mode 100644 docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md diff --git a/docs/01-getting-started/02-quick-start-guide.md b/docs/01-getting-started/02-quick-start-guide.md index 6985d6a..079460d 100644 --- a/docs/01-getting-started/02-quick-start-guide.md +++ b/docs/01-getting-started/02-quick-start-guide.md @@ -125,7 +125,7 @@ Once this association (or "link") is established, you can: A native wallet linking flow is currently in development. Once it ships, this section will document the end-to-end implementation. In the meantime, you can have users share their wallet address directly (for example, by pasting it into a form) and use that address in the steps below. :::info Reference -The [Linking User Wallets](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md) page will become the full in-depth guide once the flow ships. +The [Sending Wallet Requests](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-sending-wallet-requests.md) page will become the full in-depth guide once the flow ships. ::: ## 5. Send a Token to a User diff --git a/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-sending-wallet-requests.md b/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-sending-wallet-requests.md new file mode 100644 index 0000000..25d1860 --- /dev/null +++ b/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-sending-wallet-requests.md @@ -0,0 +1,69 @@ +--- +title: "Sending Wallet Requests" +slug: "sending-wallet-requests" +description: "Link a player's wallet to your application and send transaction requests — consume items, list on the marketplace, infuse, stake — directly to their Enjin Wallet app for approval." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +Many in-game actions need the **player** to authorize a transaction on their own wallet — consuming a potion they own, listing their loot on the marketplace, staking their ENJ for a guild perk. For these, your application can't sign with its own Wallet Daemon — the transaction has to come from the player. + +The Enjin Platform's native wallet-request flow lets your application **link a player's wallet** with their consent, and then **send transaction requests** straight to their . The player sees the request, approves or rejects it, and the platform handles the rest. + +:::warning Coming soon +The native wallet-request flow described on this page is **planned** and will be available once the Enjin Platform exits beta. The exact mutation names, argument shapes, and event payloads are not finalized yet — this page covers the flow conceptually and will be updated with code examples once the feature ships. +::: + +## How it works + +The flow has two steps: + +1. **Link the wallet.** Your application sends a one-time **connection request** to the player. They approve it in their Enjin Wallet app, and the wallet is then linked to your application on the platform. +2. **Send transaction requests.** From then on, any transaction your application creates can be targeted at the linked wallet. The player receives the request in their Enjin Wallet app, approves it (which signs it), and the transaction is broadcast on-chain. + +The link persists, so the connection step only happens once per player. Every subsequent action — consume, list, transfer, stake — is a single approval prompt in their Enjin Wallet app. + +## Step 1 — Linking a player's wallet + +Your application initiates a connection request scoped to the player. The platform delivers it to the player's Enjin Wallet app, where they see your application's name and what they're being asked to link. Once they approve, the wallet's address is returned to your application and the link is stored on the platform. + +After that, your application can: + +- Read the player's on-chain state (balance, tokens) through [`GetAccount`](/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md) using the linked address. +- Send transaction requests against the linked wallet (Step 2). + +A player can unlink at any time from their Enjin Wallet app, which revokes your application's ability to send them further requests. + +## Step 2 — Sending transaction requests + +Once a player's wallet is linked, your application can request that they sign **any** transaction the platform supports — token transfers, burns, marketplace listings, infusions, nomination-pool bonds, and so on. + +The mechanic is straightforward: when your application calls `CreateTransaction`, it sets an argument identifying the linked player as the intended signer. The transaction is built on the platform, but instead of being signed by your Wallet Daemon, it's pushed to the player's Enjin Wallet app as a signing prompt. Approving it in the app produces the signature; rejecting it cancels the transaction. + +The response your application receives includes a `uuid` for the transaction. Poll `GetTransaction(network, chain, uuid: "")` to watch its `state` move through `PENDING` → `BROADCAST` → `FINALIZED` (or `ABANDONED` if the player rejects, `TIMEOUT` if they don't respond). + +Once it reaches `FINALIZED`, the corresponding on-chain event is emitted — `MultiTokens.Burned` after a burn, `Marketplace.ListingCreated` after a listing, `NominationPools.Bonded` after a stake, and so on. See [Working with Events](/05-enjin-platform/03-working-with-events.md) for how to read them, and use the event payload to react in-game. + +## Example use cases + +The transactions a player can sign are the same ones your application's Wallet Daemon can sign — anything in the [`CreateTransaction` discriminator](/03-api-reference/02-mutations/01-transaction-mutations.md). A few examples that illustrate the gameplay loops this unlocks: + +### Consume an in-game item + +Send a request to burn a **Health Potion** NFT the player owns. On `FINALIZED`, your game reads the `MultiTokens.Burned` event, confirms the burn matched the player's wallet and the potion's token id, and restores their HP in-game. The same pattern covers any "consume" action — eat food, drink mana, use a one-shot scroll. + +### List an item on the marketplace + +Send a request to list the player's **Epic Sword** on the [Enjin Marketplace](https://nft.io/) for 300 ENJ. On `FINALIZED`, the `Marketplace.ListingCreated` event returns the new `listing_id`; your game can store it against the item and show "Listed for sale" on it in the player's inventory. + +### Infuse ENJ into a token + +Send a request to infuse 50 ENJ into the player's **Legendary Sword** to boost its in-game stats. On `FINALIZED`, the `MultiTokens.Infused` event confirms the new infusion amount, which your game reads to apply the stat boost — and the additional underlying ENJ value — to the item. + +### Stake ENJ in the game's nomination pool + +Send a request to bond 500 ENJ into your game's nomination pool to unlock a guild perk. On `FINALIZED`, the `NominationPools.Bonded` event confirms the amount; your game grants the player the perk and starts displaying their pool position in-game. + +:::info Explore More Arguments +For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. +::: diff --git a/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md b/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-using-wallet-connect.md similarity index 100% rename from docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md rename to docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-using-wallet-connect.md diff --git a/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md b/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md deleted file mode 100644 index 96c6582..0000000 --- a/docs/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md +++ /dev/null @@ -1,717 +0,0 @@ ---- -title: "Verifying Wallets" -slug: "verifying-wallets" -description: "Learn how to verify user wallets to ensure secure and trustworthy transactions within the Enjin platform, enhancing security and reliability." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Before you can interact with a user's blockchain items or associate their in-app identity with their blockchain address, you need a reliable way to confirm they own the wallet they provide. The Enjin Platform's wallet verification process provides a secure method to achieve this. - -By verifying a user's wallet, you gain certainty that the public wallet address shared genuinely belongs to them. This verified link is essential for securely accessing their blockchain inventory, delivering items, or granting ownership-based perks within your application. - -The verification process typically involves: - -1. Your application requests a unique verification code and QR code from the Enjin API. -2. The user scans the QR code with their . -3. The confirms ownership and securely provides the wallet address to the Enjin Platform. -4. Your application retrieves the verified address from the Platform. - -Once verified, you can confidently associate this blockchain address with the user's account in your system, enabling seamless blockchain interactions. - -:::tip -If you need to send requests to user wallets, use [WalletConnect](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md) instead. -::: - -:::info What you'll need: -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -::: - -## Verify Wallet with the [Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) - -### Step 1. Initiate an Account Request - -To initiate the account request process, run the following query: - -**Query:** - - - -```graphql -query RequestAccount{ - RequestAccount{ - qrCode #Returns a QR code for your user to scan - verificationId #Save this for next step - } -} -``` - - -``` -curl --location 'https://platform.beta.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query RequestAccount {\r\n RequestAccount {\r\n qrCode #Returns a QR code for your user to scan\r\n verificationId #Save this for next step\r\n }\r\n}\r\n","variables":{}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the query -var requestAccount = new RequestAccount(); - -// Define and assign the return data fragment to the query -var accountRequestFragment = new AccountRequestFragment() - .WithQrCode() - .WithVerificationId(); - -requestAccount.Fragment(accountRequestFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.beta.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendRequestAccount(requestAccount); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreQueries.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Set up the query - RequestAccount requestAccount = RequestAccount(); - - // Define and assign the return data fragments to the query - AccountRequestFragment accountRequestFragment = AccountRequestFragment() - .WithQrCode() - .WithVerificationId(); - - requestAccount.SetFragment(make_shared(accountRequestFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.beta.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendRequestAccount(*client, requestAccount); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& getAccountRequestResult = gqlResult->GetData()->GetResult(); - - std::cout << getAccountRequestResult->GetVerificationId().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.beta.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query RequestAccount{ - RequestAccount{ - qrCode #Returns a QR code for your user to scan - verificationId #Save this for next step - } - } - ` - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.beta.enjin.io/graphql', { - query: ` - query RequestAccount{ - RequestAccount{ - qrCode #Returns a QR code for your user to scan - verificationId #Save this for next step - } - } - ` -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = `''' -query RequestAccount{ - RequestAccount{ - qrCode #Returns a QR code for your user to scan - verificationId #Save this for next step - } -} -'''` - -response = requests.post('https://platform.beta.enjin.io/graphql', - json={'query': query}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -**Response:** - -```json -{ - "data": { - "RequestAccount": { - "qrCode": "https://platform.beta.enjin.io/qr?s=512&f=png&d=https://platform.beta.enjin.io/proof/MHg3OGFmYWIxYWQxMjk0YzFlZWY4MjdlNmFjNTM1MmJiOTVmNmFmNWJhNmUyNjk5ZGRkOTI3YjAwNmQ3ZDk0MzZjO2Vwc3I6QTIwMTBBSzlM", - "verificationId": "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" - } - } -} -``` - -This will return a link to a QR Code for you to display to your user, and a unique verification id to track the request. -In your application, you'll display this QR code to your user and instruct them to scan it with their . - -### Step 2: Verify Account - -To confirm that the user scanned the QR code and approved the request, you can retrieve the account using two methods. - -#### Option A: `GetWallet` - -Using `GetWallet` and passing the `verificationId` from step 1, you will be able to get any information related to his wallet if the verification process succeeded. - -**Query:** - - - -```graphql -query GetVerifiedWallet{ - GetWallet(verificationId: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"){ #Set the verificationId from the RequestAccount response - account{ - address - } - balances{ - free - } - } -} -``` - - -``` -curl --location 'https://platform.beta.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query GetVerifiedWallet($verification_id: String!) {\r\n GetWallet(verificationId: $verification_id) {\r\n account {\r\n address\r\n }\r\n balances {\r\n free\r\n }\r\n }\r\n}","variables":{"verification_id":"0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the query -var getWallet = new GetWallet() - .SetVerificationId("0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"); - -// Define and assign the return data fragment to the query -var walletFragment = new WalletFragment() - .WithAccount(new AccountFragment() - .WithAddress() - ) - .WithBalances(new BalancesFragment() - .WithFree() - ); - -getWallet.Fragment(walletFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.beta.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendGetWallet(getWallet); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); - -``` - - -```cpp -#include "EnjinPlatformSdk/CoreQueries.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Set up the query - GetWallet getWallet = GetWallet() - .SetVerificationId(make_shared("0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c")); - - // Define and assign the return data fragments to the query - AccountFragment accountFragment = AccountFragment() - .WithAddress(); - BalancesFragment balancesFragment = BalancesFragment() - .WithFree(); - WalletFragment walletFragment = WalletFragment() - .WithAccount(make_shared(accountFragment)) - .WithBalances(make_shared(balancesFragment)); - - getWallet.SetFragment(make_shared(walletFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.beta.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendGetWallet(*client, getWallet); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& getWalletResult = gqlResult->GetData()->GetResult(); - - std::cout << getWallet.GetName() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} - -``` - - -```javascript -fetch('https://platform.beta.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query GetVerifiedWallet($verification_id: String!) { - GetWallet(verificationId: $verification_id) { - account{ - address - } - balances{ - free - } - } - } - `, - variables: { - verification_id: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" //Set the verificationId from the RequestAccount response - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.beta.enjin.io/graphql', { - query: ` - query GetVerifiedWallet($verification_id: String!) { - GetWallet(verificationId: $verification_id) { - account{ - address - } - balances{ - free - } - } - } - `, - variables: { - verification_id: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" //Set the verificationId from the RequestAccount response - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -query GetVerifiedWallet($verification_id: String!) { - GetWallet(verificationId: $verification_id) { - account{ - address - } - balances{ - free - } - } -} -''' - -variables = { - 'verification_id': "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" #Set the verificationId from the RequestAccount response -} - -response = requests.post('https://platform.beta.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -**Response: If user has verified** - -```json -{ - "data": { - "GetWallet": { - "account": { - "address": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //The address of the verified account - }, - "balances": { - "free": "86010842630734264894" // The amount of free ENJ in the verified account - ~86.0108 ENJ - } - } - } -} -``` - -:::info **Formatting Balances** -The API returns balance fields in the **base unit** (u128), meaning they appear as large integers without decimals. To obtain the readable ENJ amount, **divide the returned value by 10^18**. -- **Formula:** `Base Unit Value` / `1,000,000,000,000,000,000` = `ENJ Amount` -- **Example:** A value of `86010842630734264894` converts to approximately **86.01 ENJ**. -::: - -**Response: If user has not verified** - -```json -{ - "data": { - "GetWallet": null - } -} -``` - -#### Option B: `GetAccountVerified` - -This query can be used to fetch the status of the verification, while you are showing the QR code to the player, for example, when the query returns `"verified": true` you could hide the QR code and proceed with your onboarding workflow. - -**Query:** - - - -```graphql -query GetAccountVerified{ - GetAccountVerified(verificationId: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"){ #Set the verificationId from the RequestAccount response - verified - account{ - publicKey - address - } - } -} -``` - - -``` -curl --location 'https://platform.beta.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query GetAccountVerified($verification_id: String!) {\r\n GetAccountVerified(verificationId: $verification_id) {\r\n verified\r\n account {\r\n publicKey\r\n address\r\n }\r\n }\r\n}","variables":{"verification_id":"0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the query -var getAccountVerified = new GetAccountVerified() - .SetVerificationId("0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"); - -// Define and assign the return data fragment to the query -var accountVerifiedFragment = new AccountVerifiedFragment() - .WithVerified() - .WithAccount(new AccountFragment() - .WithPublicKey() - .WithAddress() - ); - -getAccountVerified.Fragment(accountVerifiedFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.beta.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendGetAccountVerified(getAccountVerified); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreQueries.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Set up the query - GetAccountVerified getAccountVerified = GetAccountVerified() - .SetVerificationId(make_shared("0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c")); - - // Define and assign the return data fragments to the query - AccountFragment accountFragment = AccountFragment() - .WithPublicKey() - .WithAddress(); - AccountVerifiedFragment accountVerifiedFragment = AccountVerifiedFragment() - .WithVerified() - .WithAccount(make_shared(accountFragment)); - - getAccountVerified.SetFragment(make_shared(accountVerifiedFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.beta.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendGetAccountVerified(*client, getAccountVerified); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& getAccountVerifiedResult = gqlResult->GetData()->GetResult(); - - std::cout << getAccountVerifiedResult->GetVerified().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} - -``` - - -```javascript -fetch('https://platform.beta.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query GetAccountVerified($verification_id: String!) { - GetAccountVerified(verificationId: $verification_id) { - verified - account{ - publicKey - address - } - } - } - `, - variables: { - verification_id: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" //Set the verificationId from the RequestAccount response - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.beta.enjin.io/graphql', { - query: ` - query GetAccountVerified($verification_id: String!) { - GetAccountVerified(verificationId: $verification_id) { - verified - account{ - publicKey - address - } - } - } - `, - variables: { - verification_id: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" //Set the verificationId from the RequestAccount response - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -query GetAccountVerified($verification_id: String!) { - GetAccountVerified(verificationId: $verification_id) { - verified - account{ - publicKey - address - } - } -} -''' - -variables = { - 'verification_id': "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" #Set the verificationId from the RequestAccount response -} - -response = requests.post('https://platform.beta.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -**Response: If user has verified** - -```json -{ - "data": { - "GetAccountVerified": { - "verified": true, - "account": { - "publicKey": "0x5a6aae294416f3e875d9a8975658905002cfd3e5e64105d76296c4b0adbfd77e", //The public key of the verified account - "address": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //The address of the verified account - } - } - } -} -``` - -**Response: If user has not verified** - -```json -{ - "data": { - "GetAccountVerified": { - "verified": false, - "address": null - } - } -} -``` - -![Verify Flow Diagram](/img/guides/managing-users/verify-flow-diagram.png) - -:::info Explore More Arguments -For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -::: From e37b5061ebeba529e4442890a47a13b2e4582015 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 27 May 2026 16:16:04 +0300 Subject: [PATCH 52/67] Flag SDK pages as out-of-date pending v3 SDKs The C# and C++ SDK examples on the four pages under 04-software-development-kit/ all target the v2 schema and won't run against the current Enjin Platform API. v3 SDKs aren't published yet, so the SDK content is kept in place for reference but each page now opens with the standard "SDKs are not yet available" warning, pointing readers to How to Use GraphQL and the API Reference for current shapes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../04-software-development-kit/01-getting-started.md | 4 ++++ .../04-software-development-kit/02-graphql-requests.md | 4 ++++ .../04-software-development-kit/03-platform-events.md | 4 ++++ .../04-software-development-kit.md | 5 +++++ 4 files changed, 17 insertions(+) diff --git a/docs/02-guides/01-platform/04-software-development-kit/01-getting-started.md b/docs/02-guides/01-platform/04-software-development-kit/01-getting-started.md index 7e86e03..c2d16cf 100644 --- a/docs/02-guides/01-platform/04-software-development-kit/01-getting-started.md +++ b/docs/02-guides/01-platform/04-software-development-kit/01-getting-started.md @@ -7,6 +7,10 @@ description: "Learn how to get started with the Enjin SDK, your first step towar import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +:::warning SDKs are not yet available +The C# and C++ SDK content on this page is out of date and **will not work against the current Enjin Platform API**. This page will be updated once new SDKs are published. Until then, see [How to Use GraphQL](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md) and the [API Reference](/03-api-reference/03-api-reference.md) for the current API shapes. +::: + To have the SDK connect with the Enjin Platform we must use its platform clients and event services. As a starter, we will look over how to set up these clients. :::warning Please always make sure to integrate authentication endpoints via secure backend servers. diff --git a/docs/02-guides/01-platform/04-software-development-kit/02-graphql-requests.md b/docs/02-guides/01-platform/04-software-development-kit/02-graphql-requests.md index b08b74f..4e94666 100644 --- a/docs/02-guides/01-platform/04-software-development-kit/02-graphql-requests.md +++ b/docs/02-guides/01-platform/04-software-development-kit/02-graphql-requests.md @@ -7,6 +7,10 @@ description: "Learn how to use GraphQL requests to interact with the Enjin platf import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +:::warning SDKs are not yet available +The C# and C++ SDK content on this page is out of date and **will not work against the current Enjin Platform API**. This page will be updated once new SDKs are published. Until then, see [How to Use GraphQL](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md) and the [API Reference](/03-api-reference/03-api-reference.md) for the current API shapes. +::: + ## Creating the Request The most important step in sending a request is to first create said request. The SDK provides us with two ways to create a request. The first way is to utilize a predefined, dynamic request class to programmatically compile our request at runtime. The second way is to provide our own query string to be sent along with any relevant information, however, this method of going about creating a request places a greater demand on us to validate that our queries and mutations are indeed correct. diff --git a/docs/02-guides/01-platform/04-software-development-kit/03-platform-events.md b/docs/02-guides/01-platform/04-software-development-kit/03-platform-events.md index 9157055..7c52d04 100644 --- a/docs/02-guides/01-platform/04-software-development-kit/03-platform-events.md +++ b/docs/02-guides/01-platform/04-software-development-kit/03-platform-events.md @@ -7,6 +7,10 @@ description: "Explore how to monitor and handle platform events in the Enjin SDK import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +:::warning SDKs are not yet available +The C# and C++ SDK content on this page is out of date and **will not work against the current Enjin Platform API**. This page will be updated once new SDKs are published. Until then, see [How to Use GraphQL](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md) and the [API Reference](/03-api-reference/03-api-reference.md) for the current API shapes. +::: + ## Event Listeners To allow our application to receive events from the platform the SDK comes with an `IEventListener` interface for us to use to receive events for processing. diff --git a/docs/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md b/docs/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md index f80faf4..72a809e 100644 --- a/docs/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md +++ b/docs/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md @@ -3,6 +3,11 @@ title: "Software Development Kits (SDKs)" slug: "../software-development-kit" description: "Get started with the Enjin SDK, offering comprehensive developer tools to integrate blockchain technology into your apps and games with ease." --- + +:::warning SDKs are not yet available +The C# and C++ SDK content on this page is out of date and **will not work against the current Enjin Platform API**. This page will be updated once new SDKs are published. Until then, see [How to Use GraphQL](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md) and the [API Reference](/03-api-reference/03-api-reference.md) for the current API shapes. +::: + The Enjin Platform SDKs provide an API for communicating with the Enjin Platform. The Enjin Platform SDKs provide a framework that developers can use on their desired programming language as it allows games and apps to interact with the Enjin API without the need of building a bridge between your application and our API requests, the Enjin SDKs facilitate the usage of listening and processing of events emitted by the Enjin Platform, which helps us deliver a better user experience in our application and eliminate unnecessary API requests we would otherwise make to the Enjin Platform. From e7dd0b8a5c08416f67a90e2c3758d7094e07f727 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 27 May 2026 16:47:41 +0300 Subject: [PATCH 53/67] Rewrite the Enjin Platform overview page for v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reframes the top-level platform page around the v3 reality — a single hosted GraphQL API reached via one endpoint with a per-request network selector, not the v2 "open-source framework with optional packages" model. Drops the three-versions callout, the v2 mutation screenshot, and the v2 dashboard screenshot (with Beams in the sidebar) — keeps the still-accurate architecture diagram and points at the migrated using-the-platform page for current dashboard visuals. The Notable Platform Features list is curated to the v3 surface: single-endpoint multi-chain routing, managed wallets, wallet linking and transaction requests (forward-pointer to sending-wallet-requests), asset management with CreateBatchTransaction, marketplace, fuel-tank dispatching, token groups, and direct Relaychain support (nomination-pool participation). Drops the v2-era Data Management Tool, Quick Sync, Auto-Healing, and Modular Design / Laravel Horizon subsections — internal-architecture and open-source-only framing that no longer applies. Auto-teleport stays. Enjin Platform Cloud section reframed from "the easy variant" to "fully hosted, no servers to maintain, your keys stay yours." Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/05-enjin-platform/05-enjin-platform.md | 90 +++++++-------------- 1 file changed, 28 insertions(+), 62 deletions(-) diff --git a/docs/05-enjin-platform/05-enjin-platform.md b/docs/05-enjin-platform/05-enjin-platform.md index e013b27..3cefd13 100644 --- a/docs/05-enjin-platform/05-enjin-platform.md +++ b/docs/05-enjin-platform/05-enjin-platform.md @@ -1,102 +1,68 @@ --- title: "Enjin Platform" slug: "../enjin-platform" -description: "Discover the Enjin Platform, a comprehensive suite of tools for building, managing, and integrating blockchain-based assets in your projects." +description: "The Enjin Platform is a cloud-based GraphQL API for building, managing, and integrating blockchain-based assets in your applications." --- -:::info There are three versions of the Enjin Platform to choose from: -- The [Enjin Platform Cloud (Testnet)](https://platform.canary.enjin.io/login), connected to Enjin Canary Testnet -- The [Enjin Platform Cloud (Mainnet)](http://platform.beta.enjin.io/), connected to Enjin Blockchain Mainnet -::: -## What is the Enjin Platform? - -The Enjin Platform is the world’s most powerful and advanced open-source framework for building NFT platforms and integrations. It allows developers and their projects to easily communicate with the Enjin Blockchain, forming a light-weight and modular layer between the blockchain, and their applications. +The Enjin Platform is the bridge between your application and the Enjin Blockchain. -It's the bridge between the complex world of blockchain and user-friendly applications, minimizing the technical barriers usually associated with blockchain integration. This is achieved through a lightweight, modular layer that uses a modern, flexible GraphQL API. This layer manages all blockchain interactions, thus removing the necessity of engaging with traditional smart contracts directly. +## What is the Enjin Platform? -Consider the Enjin Platform as your digital interpreter, simplifying your communication with the blockchain. It enables you to create collections, mint and transfer blockchain tokens, and even administer blockchain collections and wallets with ease. +The Enjin Platform is a cloud-based GraphQL API for building NFT-powered applications. It removes the complexity of encoding extrinsics, managing nonces, and signing transactions — letting you mint, transfer, and manage on-chain assets through clean, high-level mutations. -The Enjin Platform works much like a customizable building set. It has a modular design, which means you can add or take away pieces as needed. These pieces come in the form of optional add-on packages. They provide extra tools, such as managing Fuel Tanks, handling marketplace listings (through the marketplace package), or generating Enjin Beams via the API (with the Enjin Beam package). In other words, with the Enjin Platform, you get a customized setup that fits your specific needs. +Both Enjin Mainnet and Canary Testnet are reached through a single endpoint, selected per request. Whether you're prototyping on Canary or shipping on Mainnet, you use the same API token and the same queries. -![](/img/components/4.png) +Think of the platform as your digital interpreter — simplifying communication with the chain so your application can focus on its product (game mechanics, marketplace UX, or whatever your project does best). ## How does it work? -How does Enjin Platform bring all these functionalities together? The answer lies in its structure. The platform comprises distinct components each designed to offer a unique solution for developers and creators. +The platform comprises three main components, each handling a different layer of the integration.

-### API & Schema +### GraphQL API -The [Open-Source Enjin Platform](https://github.com/enjin/platform) also provides a ready-made application that simplifies blockchain actions for developers by removing the need for encoding, signing transactions, and understanding complex technical details. +Our [GraphQL API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) is the entry point for every interaction. It provides: -The project has two key components: - -- **Enjin API:** Our GraphQL API provides a server-based interface that allows you to execute a range of queries and mutations. In other words, you can easily retrieve necessary information or execute transactions on the blockchain. -- **Events:** With the Enjin Platform, developers can subscribe to events and receive real-time updates on any on-chain activities, eliminating the need to constantly poll the API for changes. +- **Queries** for reading blockchain state — accounts, tokens, collections, marketplace listings, nomination pools, and transactions. +- **Mutations** for building and broadcasting transactions. All on-chain actions go through a single `CreateTransaction` mutation, with the specific action selected via the `transaction` input (e.g. `createCollection`, `mintToken`, `transferToken`). +- **Events** for real-time updates — subscribe to receive on-chain events as they fire, instead of polling. See [Working with Events](/05-enjin-platform/03-working-with-events.md) for the current event-reading flow. ### Wallet Daemon -The [Wallet Daemon](https://github.com/enjin/wallet-daemon) is the component responsible for automatically signing and broadcasting transactions to the Enjin Blockchain. Running on your own secure server, the Wallet Daemon removes the need for manual signing of each transaction by polling the Enjin Platform in 6-second intervals, fetching new transaction requests, signing them with your private key, and submitting them to a blockchain node. As the Wallet Daemon keeps track of the nonce locally, it can process and submit more than one transaction per block. +The [Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) is the signing service for your project. It runs on your own infrastructure, polls the platform for queued transactions, signs them with your private key, and submits them to the chain. Because it tracks nonce locally, it can submit multiple transactions per block. ### SDKs -With the release of the Enjin Platform, we’re preparing multiple SDKs to further streamline integration into your projects. Our first release includes the open-source [C# SDK](https://github.com/enjin/platform-csharp-sdk) for connecting and interacting with the Enjin Platform. - -Want to learn more about our SDKs? Dive into our documentation [here](/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md). +SDKs make integrating the platform into your application easier by wrapping the GraphQL API in native libraries. The first releases are the [C# SDK](https://github.com/enjin/platform-csharp-sdk) and [C++ SDK](https://github.com/enjin/platform-cpp-sdk) — see the [Software Development Kits page](/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md) for usage. ## Notable Platform Features The Enjin Platform is packed with features designed to make your life as a developer easier. Here's a glimpse of what's in store: -### Enabled Automation - -- **User-validation requests:** A swift way to establish user identities -- **Authentication requests:** Seamless process to secure sessions -- **Managed wallet creation:** Forget the complex coding, create wallets with a few clicks -- **Wallet linking and switching:** Easily manage multiple wallets and switch between them -- **Asset transfer and management:** Smoothly transfer and manage your digital assets -- **Enjin Beam support:** Leverage Enjin Beams for unique offerings -- **Fuel Tanks support:** Efficiently manage your fuel tanks for subsidizing transaction fees -- **Marketplace support:** Create and manage marketplace listings with ease -- **Relaychain auto-teleport:** Any Relaychain ENJ mistakenly sent to managed wallet will be automatically teleported to Matrixchain - -### Data Management Tool - -Simplify data organization with our Data Management Tool. Provided by Indexers, it structures and organizes blockchain data by creating a centralized database from blockchain inputs. This means you, as a developer, can access specific data without sifting through the entire blockchain database. - -### Quick Sync Method +- **Single endpoint, multi-chain routing** — reach both Enjin Mainnet and Canary Testnet, and target either the Matrixchain or the Relaychain, through one URL by changing a single argument on the request. +- **Managed wallet creation** — create platform-side wallets for your end users with [`CreateManagedWallet`](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md), and have your project's Wallet Daemon sign on their behalf. +- **Wallet linking and transaction requests** — [link a player's Enjin Wallet](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-sending-wallet-requests.md) to your application and send transaction-signing prompts directly to it. Use it for ownership verification, in-app authentication, or approving in-game actions like consuming items, listing on the marketplace, or staking ENJ. +- **Asset transfer and management** — transfer, mint, freeze, burn, and infuse tokens through the API. Bundle multiple actions into a single `CreateBatchTransaction` to commit them atomically. +- **Marketplace** — create [fixed-price, auction, and offer listings](/03-api-reference/02-mutations/06-marketplace-mutations.md), place bids, and answer counter-offers. +- **Fuel Tank dispatching** — dispatch transactions through an existing [Fuel Tank](/04-enjin-blockchain/03-enjin-matrixchain/02-fuel-tank-pallet.md) to subsidize transaction fees for your users. +- **Token Groups** — organize tokens within a collection into named groups for in-game categorization or access control. See [Token Groups mutations](/03-api-reference/02-mutations/07-token-groups-mutations.md). +- **Relaychain support** — stake ENJ into [nomination pools](/03-api-reference/02-mutations/08-nomination-pool-mutations.md) for staking rewards, directly through the platform. -The Enjin Platform offers a quick sync method that enables developers to fetch and store the most recent state of the blockchain in a pre-packaged index. This saves time and effort that would otherwise be spent on syncing the entire blockchain. The "Worker" service oversees and manages all the blocks produced in the blockchain and parses all extrinsic events in each block. +### Platform Dashboard -### Auto-Healing Feature - -The Enjin Platform includes an auto-healing feature that continuously monitors the health of the database. If any issues arise, such as corruption or a saving mismatch, the platform automatically re-syncs with the current chain state to ensure that all data is correct and up-to-date. +The Enjin Platform comes with a [browser-based dashboard](/01-getting-started/04-using-the-enjin-platform.md) for creating, viewing, and managing collections, tokens, marketplace listings, and transactions — all without writing code. It's a fast way to prototype before committing to API integration, and stays useful as an admin tool once your application is live. ### Relaychain ENJ Auto-teleport -The Enjin Ecosystem is composed of two distinct chains: the Enjin Relaychain, which handles Staking and Governance, and the Enjin Matrixchain, a hub for NFTs and marketplace functionalities. There can be a potential misunderstanding when users purchase ENJ on an exchange and deposit it into a Dapp's managed wallet on the Relaychain. This is because the Dapp creator anticipates the deposit on the Matrixchain, not the Relaychain. To address this, we've introduced the Auto-Teleport feature. This feature automatically teleports any Relaychain ENJ received in a managed wallet to the corresponding Matrixchain. It streamlines the process, eliminates room for error and the need for manual teleportation. - -### Modular Design - -The Enjin Platform is designed with a light, modular structure, allowing developers to custom-fit the integration depending on their requirements and design philosophy. The Platform and Platform Core provide fundamental functionality, while multiple optional packages are available to expand your integration. You can enhance the Platform's capabilities without modifying the core codebase. - -Developers may further augment their Enjin Platform configuration by integrating [Laravel Horizon](https://laravel.com/docs/10.x/horizon), translating all Platform data into a visually appealing dashboard. - -Everyone is welcome to contribute to the codebase and build upon the framework. Don’t forget to [Follow Enjin on GitHub](https://github.com/enjin)! - -### Platform User Interface - -The Enjin Platform comes with a lightweight and easy-to-use user interface. The graphical interface comes with GUI forms for creating, viewing, managing collections & NFTs. You can also extend the UI with additional components like Beams and Fuel Tanks, depending on their availability and installation. - -![](/img/components/6.png) +The Enjin Ecosystem is composed of two distinct chains: the Enjin Relaychain (which handles staking and governance) and the Enjin Matrixchain (a hub for NFTs and marketplace functionality). When users purchase ENJ on an exchange and deposit it into a Dapp's managed wallet, they sometimes send it to the Relaychain address — even though the Dapp expects the deposit on the Matrixchain. The Auto-teleport feature solves this: any Relaychain ENJ received in a managed wallet is automatically teleported to the corresponding Matrixchain address, removing room for error and the need for manual teleportation. ## Enjin Platform Cloud -The modularity and flexibility of the Enjin Platform can be daunting for first-timers - which is exactly why we’ve rolled out the Enjin Platform Cloud! +The Enjin Platform is fully hosted, so there's nothing to install, deploy, or maintain on your side. Sign up, generate an API token, and start building — the platform handles node connectivity, indexing, scaling, and reliability for you. -This user-friendly version allows you to kickstart your next NFT project in just a few minutes. Ran on Enjin's robust servers, yet fully secure and isolated to your unique project instance, you can launch your NFT game with a few simple steps. Just create an account, set it up, generate your first API key, and voila! Your NFT game is operational. The [Wallet Daemon](https://github.com/enjin/wallet-daemon) on your own server securely handles the signing and broadcasting of all transactions, ensuring the utmost integrity of your project's security. +Each project runs in its own isolated context on Enjin's infrastructure, with your API token scoping access. Signing stays under your control: a [Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) that you run holds your private keys and signs transactions before submitting them through the platform — keys never leave your environment. -Get started today on [mainnet](https://platform.beta.enjin.io/register) or [canary](https://platform.canary.enjin.io/register). +[Create an account](https://platform.beta.enjin.io/register) to get started. From 6a5685e576da66ac06b51017ad37072637fc70c5 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 27 May 2026 17:09:45 +0300 Subject: [PATCH 54/67] Migrate three advanced-mechanics pages to v3 - TokenID Structure: drop the platform-side encoder section (ERC1155 / Hash / StringId / Integer wrappers gone in v3); replace with a short "Passing a Token ID to the API" note explaining that tokenId is now a flat BigInt and the encoding is done client-side. Add Token Groups pointer for shared-metadata grouping. - Brand Collab NFTs: rewrite the Development section around GetAccount (GetWallet/tokenAccounts removed in v3); use the flat Account.tokens list, filter client-side by collection.id. Trim Best Practices to the bullets the v3 API still supports (per-token-balance and reserved balance not currently exposed). Keep the ERC-1155 Grouped NFTs section and add a Token Groups pointer for new projects. - ENJ Infusion: rewrite around the createToken / mutateToken / infuseToken discriminators on CreateTransaction. Drop the v2 EncodableTokenIdInput wrapper, the console.enjin.io workaround (mutateToken now accepts anyoneCanInfuse), and the bare Authorization header. Add the SDKs-not-available callout above the language tabs and the standard MultiTokens.Infused event-mention forward pointer. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../01-tokenid-structure.md | 105 +++------- .../04-brand-collab-nfts.md | 115 +++++------ .../03-advanced-mechanics/05-enj-infusion.md | 182 ++++++++++++------ 3 files changed, 190 insertions(+), 212 deletions(-) diff --git a/docs/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md b/docs/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md index 1b01b7d..7d6af22 100644 --- a/docs/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md +++ b/docs/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md @@ -7,7 +7,7 @@ description: "Learn about the Enjin TokenID structure, its format, and how to ut Token IDs in a collection serve as unique identifiers for tokens, represented as 128-bit integers. Beyond their primary role, token IDs can also store structured information about a token's attributes, providing greater organizational flexibility. -This guide outlines recommended approaches for structuring and organizing token IDs, as well as the four encoding options available on the Enjin Platform: **ERC1155**, **Hash**, **StringId**, and **Integer** (no encoding). +This guide outlines four common approaches for structuring and organizing token IDs — **Bitmasks**, **Ranges**, **Hashes**, and **Integer** — along with how to pass them to the platform. *** @@ -93,96 +93,37 @@ Token IDs are manually assigned, such as `1`, `2`, `3`, etc. This method is stra *** -## Enjin Platform Token ID Encoders +## Passing a Token ID to the API -The Enjin Platform provides built-in encoders to convert data into 128-bit token IDs: - -### 1. **ERC1155 Encoder** - -This encoder uses a 16-bit hexadecimal token ID and a 64-bit integer index. - -```graphql -tokenId: { - erc1155: { - tokenId: "0x78c0000000003377", index: 10 - } -} -``` - -Resulting Token ID: `160504280491028834688987873652194148362` - -*** - -### 2. **Hash Encoder** - -This encoder generates a token ID from a hashed array or object. - -for example: +`tokenId` is a flat `BigInt` scalar — submit the structured 128-bit integer directly, with no wrapper object: ```graphql -tokenId: { - hash: { - Token: "Sword", Number: 5 - } +mutation { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + mintToken: { + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" + collectionId: 36105 + tokenId: 160504280491028834688987873652194148362 + amount: 1 + } + } + ) { uuid action state } } ``` -Resulting Token ID: `65304702016350863193892557847492631289` - -Token IDs generated with this method cannot be reversed or decoded. +If you want to encode structure into the token ID (bitmask layout, hash, string-derived ID, etc.), compute the resulting 128-bit integer in your application and pass it as `tokenId`. The platform does not perform the encoding for you. -:::info Irreversible Token IDs with the Hash Encoder -Token IDs generated using the Hash Encoder are derived from a cryptographic hash function. As hashing is a one-way process, these token IDs cannot be reversed or decoded back into the original data. +:::tip Need to group tokens by type? +If your goal is to share metadata or utility across a group of tokens — the use case the old ERC-1155 encoder addressed — use the built-in **Token Groups** feature instead of embedding the group in the token ID. See [Token Groups mutations](/03-api-reference/02-mutations/07-token-groups-mutations.md). ::: -:::danger Important Note -The Hash Encoder generates token IDs by hashing input data. If the resulting hash exceeds 128 bits, the encoder will fail. -::: - -*** - -### 3. **StringId Encoder** - -This encoder converts a string into a numeric token ID. - -for example: - -```graphql -tokenId: { - stringId: "MyToken-0001" -} -``` - -Resulting Token ID: `23977024514528806328972881969` - -:::danger Important Note -The StringId Encoder converts strings into numeric token IDs. Strings that produce integers larger than 128 bits will cause the encoding to fail. -::: - -*** - -### 4. **Integer** - -For direct assignment, integers can be passed as-is to create token IDs. - -for example: - -```graphql -tokenId: {integer: 1001} -``` - -Resulting Token ID: `1001` - -This approach is simple and requires no encoding or decoding. - -*** - ## Choosing the Right Approach -### Considerations: - -- **Ease of Use**: Use Integers or ranges for simplicity. -- **Scalability**: Bitmasks and hashes work best for large collections with many attributes. -- **Interoperability**: Use the ERC1155 encoder for compatibility with Ethereum-based assets. +- **Ease of Use** — Plain integers or ranges keep IDs human-readable and easy to manage. +- **Scalability** — Bitmasks and hashes work well for large collections with many attributes. +- **Grouping** — For shared metadata across many tokens, prefer on-chain [Token Groups](/03-api-reference/02-mutations/07-token-groups-mutations.md) over packing the group into the token ID. -By understanding these methods and tools, you can design token ID structures that best suit your project's needs while ensuring flexibility and scalability. +By picking the structure that best fits your project, you can design a token-ID layout that stays flexible and scalable as your collection grows. diff --git a/docs/02-guides/01-platform/03-advanced-mechanics/04-brand-collab-nfts.md b/docs/02-guides/01-platform/03-advanced-mechanics/04-brand-collab-nfts.md index c73f813..0e7f9a2 100644 --- a/docs/02-guides/01-platform/03-advanced-mechanics/04-brand-collab-nfts.md +++ b/docs/02-guides/01-platform/03-advanced-mechanics/04-brand-collab-nfts.md @@ -76,108 +76,77 @@ By marking the utility of your game on the token’s metadata as "locked," and c ## Development -Here’s the Query you will need to use: +Reading the tokens a player holds is done with the `GetAccount` query — see [Reading User Wallets](/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md) for the canonical query shape and response structure. + +Each token in the returned `tokens` list carries its `collection.id` and `tokenId`, so identifying brand or multiverse items the player owns is a matter of matching those values against the IDs your game cares about. ```graphql -query GetWallet { - GetWallet(account:"efUVQhs3Q1ih9RKn1ukpb45p2Zomu5jvopsQE6n5LtW52FJH3") { -// omit “tokenIds” to read all tokens in a collection -// if you are getting a page after the first put the “endCursor” value into the “after” field -tokenAccounts (collectionIds:2100 tokenIds:1 after:"" first:100) { - pageInfo { - hasNextPage - endCursor - } - edges{ - node { - balance - reservedBalance - token{ - tokenId - attributes { - key - value - } - } - } - } +query ScanForBrandItems { + GetAccount( + network: ENJIN # or CANARY for testnet + chain: MATRIX + address: "efUVQhs3Q1ih9RKn1ukpb45p2Zomu5jvopsQE6n5LtW52FJH3" + ) { + tokens { + id # canonical "-" + tokenId + collection { id } + attributes { key value } } } } - ``` ``` -curl -X POST "https://platform.beta.enjin.io/graphql" \ --H "Content-Type: application/json" \ --H "Authorization: enjin_api_key" \ --d '{ - "query": "query GetWallet { GetWallet(account: \"efUVQhs3Q1ih9RKn1ukpb45p2Zomu5jvopsQE6n5LtW52FJH3\") { tokenAccounts(collectionIds: 2100, tokenIds: 1, after: \"\", first: 100) { pageInfo { hasNextPage endCursor } edges { node { balance reservedBalance token { tokenId attributes { key value } } } } } } }" -}' +curl --location 'https://platform.beta.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"query ScanForBrandItems($address: String!) {\r\n GetAccount(network: ENJIN, chain: MATRIX, address: $address) {\r\n tokens {\r\n id\r\n tokenId\r\n collection { id }\r\n attributes { key value }\r\n }\r\n }\r\n}","variables":{"address":"efUVQhs3Q1ih9RKn1ukpb45p2Zomu5jvopsQE6n5LtW52FJH3"}}' ``` -Here’s an example of a response you can expect. +The response includes every token the wallet currently holds. Filter it client-side by `collection.id` (and optionally `tokenId`) to pick out the brand-collab items your game integrates: ```json { "data": { - "GetWallet": { - "tokenAccounts": { - // if there are more items to read there will be a guid in the “endCursor” value - "pageInfo": { - "hasNextPage": false, - "endCursor": "" - }, - "edges": [ - { - "node": { - "balance": "3", - "reservedBalance": "0", - "token": { - "tokenId": "1", - // to read the metadata I should query the url provided as the value for the uri attribute - "attributes": [ - { - "key": "uri", - "value": "https://etherscapegame.com/crypto/ImperialSovereign.json" - } - ] - } + "GetAccount": { + "tokens": [ + { + "id": "2100-1", + "tokenId": "1", + "collection": { "id": "2100" }, + "attributes": [ + { + "key": "uri", + "value": "https://etherscapegame.com/crypto/ImperialSovereign.json" } - } - ] - } + ] + } + ] } } } - ``` +To read the external metadata, fetch the URL listed under the token's `uri` attribute. + ## Best Practices :::tip Tips -- Only pull data from the collections you need. This will improve performance and ensure your backend infrastructure remains scalable. -- Always read and check the balance. Sometimes, the player may hold the token but the balance is actually zero, for instance, if the token has been burned. These burned tokens should not have in-game utility. -- Consider whether you want NFTs that are listed on a marketplace to have utility in your game. Items that are listed for sale don't show up in regular token balance and show up in reserved balance instead. If you want them to have utility, you can check the reserved balance and include the reserved supply. -- Consider if you need to use pagination. Users can have hundreds of tokens, in this situation you will need to read them in multiple calls. -- If you plan to utilize metadata from on-chain or external sources, it's important to also read the token's attributes. Typically, you'll find a "uri" attribute that points to the external location of this metadata. -- When accessing external metadata or media, make sure to do so asynchronously and think about storing it in a local cache for faster retrieval. +- A token appears in a wallet's `tokens` list only while the wallet holds a balance of it, so the list always reflects current holdings — no need to filter out burned tokens. +- If you plan to utilize metadata from on-chain or external sources, also read the token's `attributes`. Typically you'll find a `uri` attribute that points to the external location of this metadata. +- When accessing external metadata or media, make the calls asynchronously and consider storing them in a local cache for faster retrieval. ::: ### Syncing Tokens -In order to implement any token in game you will need to sync a list of token ids that your player has in their wallet. In the most simple case, you only need to sync tokens from a single collection (likely the main collection associated with your game). However, in multiverse cases, you will now need to sync tokens from multiple collections. There are a couple of options to achieve this. - -1. Sync All Tokens - The first approach is to sync all the tokens in the players wallet across all collections. This makes the graphQL query very simple. However, with this approach you need to bear in mind that some wallets will have thousands of tokens in them (and could grow to even hundreds of thousands). This means that you can’t rely on syncing all the wallets tokens in a single call. You will need to make sure to implement proper pagination using the GraphQL API’s so that you can read out the tokens in many batches. -2. Sync Specific Collections Only - Another approach is to only sync the collections you care about. This will limit the total number of tokens that you need to sync. In your GraphQL query you can specify a list of collection and token ids in order to narrow down the range. Depending on your case, you may still want to handle pagination of calls if any of the collections you include have a large number of tokens. +To implement any token in game you'll need to know which token IDs the player holds. `GetAccount.tokens` returns every token across every collection the wallet holds in a single response — filter the list client-side down to the collection IDs your game cares about (for example, the main collection associated with your game plus any brand-collab collections you integrate). ### Syncing Metadata @@ -199,9 +168,9 @@ Note that if you’re adopting another game’s token, you may not want to use a ### ERC-1155 Grouped NFTs -There are many tokens that have been migrated from the ERC-1155 version of Enjin. In ERC-1155 there is a concept called “Grouped NFTs” where there is a grouping of NFT tokens that share the same metadata. +There are many tokens that have been migrated from the ERC-1155 version of Enjin. In ERC-1155 there is a concept called "Grouped NFTs" where a set of NFT tokens share the same metadata. -A popular example of this would be “Oindrasdain” from “The Multiverse” collection. There are many copies of Oindrasdain and they have token ids that look like the following… +A popular example of this would be "Oindrasdain" from "The Multiverse" collection. There are many copies of Oindrasdain and they have token ids that look like the following… `107002853660685728488179487227240257220` @@ -216,3 +185,7 @@ Group ID: `5080000000000027` Group Index: `00000000000016C4` All tokens in the collection that share the same group id are intended to be of the same token type and thus should get the same utility. This is similar to how s work. + +:::tip Token Groups +For projects starting fresh, the recommended way to group tokens that share metadata or utility is the on-chain **Token Groups** feature, not packing a group ID into the token ID. Many migrated collections (including The Multiverse) have also been organized into on-chain Token Groups, so you can read group membership through the API instead of decoding the 128-bit layout. See [Token Groups mutations](/03-api-reference/02-mutations/07-token-groups-mutations.md) and [Token Groups queries](/03-api-reference/01-queries/07-token-groups-queries.md). +::: diff --git a/docs/02-guides/01-platform/03-advanced-mechanics/05-enj-infusion.md b/docs/02-guides/01-platform/03-advanced-mechanics/05-enj-infusion.md index de0956c..b228cc1 100644 --- a/docs/02-guides/01-platform/03-advanced-mechanics/05-enj-infusion.md +++ b/docs/02-guides/01-platform/03-advanced-mechanics/05-enj-infusion.md @@ -35,8 +35,29 @@ By integrating ENJ Infusion, creators and developers can provide enhanced value ## Creating a Token with Infused ENJ -To create a token with Infused ENJ, proceed with the instructions on the [Creating Tokens](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) page, but make sure to specify the amount of ENJ to infuse into the token, in the “Infuse ENJ” section. -If you are using the `CreateToken` mutation, insert the infusion amount in the `params: infusion` parameter. +To create a token with Infused ENJ, follow the [Creating Tokens](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) guide and set the **Infuse ENJ** amount on the token creation form. + +When creating the token through the API, set the `infusion` field on the `createToken` action of `CreateTransaction`: + +```graphql +mutation { + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + createToken: { + recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" + collectionId: 3298 + tokenId: 1 + initialSupply: 1 + listingForbidden: false + infusion: 5000000000000000000 # 5 ENJ in base units + anyoneCanInfuse: false + } + } + ) { uuid action state } +} +``` :::warning Calculating the infusion amount The Platform accepts infusion values in the **base unit** (integers), not decimal ENJ amounts. To calculate the correct input, **multiply your desired ENJ amount by 10^18** (1 quintillion). @@ -46,20 +67,29 @@ The Platform accepts infusion values in the **base unit** (integers), not decima ### Anyone Can Infuse -By default, ENJ infusion is restricted only to the collection owner +By default, ENJ infusion is restricted to the collection owner. Setting `anyoneCanInfuse: true` lets any account add ENJ infusion to the token. -At any time, as well as at the time of creating the token, the collection owner can select to to allow anyone to add ENJ infusion to the token. -This can be done with the `CreateToken` mutation by setting the `params: anyoneCanInfuse` parameter to `true`. -If the token is already created, the `anyoneCanInfuse` state can be adjusted via the `MutateToken` mutation, with the `mutation: anyoneCanInfuse` parameter. +This can be set at token creation via the `createToken` action above, or toggled on an existing token with the `mutateToken` action: -:::warning **Feature Availability Notice** -Currently, the `anyoneCanInfuse` state can only be configured at the time of creating the token. The option to adjust the infusion permission for an existing token via a mutation will be added later on. We will update the documentation once this feature is available. -In the meantime, if you wish to adjust infusion permission of an existing token now, please use the Enjin Console at [console.enjin.io](https://console.enjin.io). -::: +```graphql +mutation { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + mutateToken: { + collectionId: 3298 + tokenId: 1 + anyoneCanInfuse: true + } + } + ) { uuid action state } +} +``` -## Infusing ENJ to existing token +## Infusing ENJ to an existing token -To add ENJ infusion to a token that already exists, use the `Infuse` mutation: +To add ENJ infusion to a token that already exists, use the `infuseToken` action on `CreateTransaction`: :::warning Calculating the infusion amount The Platform accepts infusion values in the **base unit** (integers), not decimal ENJ amounts. To calculate the correct input, **multiply your desired ENJ amount by 10^18** (1 quintillion). @@ -67,17 +97,27 @@ The Platform accepts infusion values in the **base unit** (integers), not decima - **Example:** To infuse a token with **5 ENJ**, input `5000000000000000000`. ::: +:::warning SDKs are not yet available +The C# and C++ SDK examples below are out of date and **will not work against the current Enjin Platform API**. This section will be updated once new SDKs are published. Until then, use the GraphQL, cURL, Javascript, Node.js, or Python examples. +::: + ```graphql -mutation Infuse{ - Infuse( - collectionId: 3298 #Specify the collection ID - tokenId: {integer: 1} #Specify the token ID - amount: 5000000000000000000 #Specify the amount of ENJ to infuse - ){ - id - method +mutation InfuseToken { + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + infuseToken: { + collectionId: 3298 + tokenId: 1 + amount: 5000000000000000000 # 5 ENJ in base units + } + } + ) { + uuid + action state } } @@ -87,8 +127,8 @@ mutation Infuse{ ``` curl --location 'https://platform.beta.enjin.io/graphql' \ -H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation Infuse(\r\n $collection_id: BigInt!\r\n $token_id: EncodableTokenIdInput!\r\n $amount: BigInt!\r\n) {\r\n Infuse(collectionId: $collection_id, tokenId: $token_id, amount: $amount) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":3298,"token_id":{"integer":1},"amount":5000000000000000000}}' +-H 'Authorization: Bearer YOUR_API_TOKEN' \ +-d '{"query":"mutation InfuseToken($collectionId: BigInt!, $tokenId: BigInt!, $amount: BigInt!) {\r\n CreateTransaction(\r\n network: ENJIN\r\n chain: MATRIX\r\n transaction: {\r\n infuseToken: {\r\n collectionId: $collectionId\r\n tokenId: $tokenId\r\n amount: $amount\r\n }\r\n }\r\n ) {\r\n uuid\r\n action\r\n state\r\n }\r\n}","variables":{"collectionId":3298,"tokenId":1,"amount":"5000000000000000000"}}' ``` @@ -132,25 +172,31 @@ Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); ```javascript fetch('https://platform.beta.enjin.io/graphql', { method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'}, body: JSON.stringify({ query: ` - mutation Infuse( - $collection_id: BigInt! - $token_id: EncodableTokenIdInput! - $amount: BigInt! - ) { - Infuse(collectionId: $collection_id, tokenId: $token_id, amount: $amount) { - id - method + mutation InfuseToken($collectionId: BigInt!, $tokenId: BigInt!, $amount: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + infuseToken: { + collectionId: $collectionId + tokenId: $tokenId + amount: $amount + } + } + ) { + uuid + action state } } `, variables: { - collection_id: 3298, //Specify the collection ID - token_id: {integer: 1}, //Specify the token ID - amount: 5000000000000000000 //Specify the amount of ENJ to infuse + collectionId: 3298, + tokenId: 1, + amount: "5000000000000000000" } }), }) @@ -164,25 +210,31 @@ const axios = require('axios'); axios.post('https://platform.beta.enjin.io/graphql', { query: ` - mutation Infuse( - $collection_id: BigInt! - $token_id: EncodableTokenIdInput! - $amount: BigInt! - ) { - Infuse(collectionId: $collection_id, tokenId: $token_id, amount: $amount) { - id - method + mutation InfuseToken($collectionId: BigInt!, $tokenId: BigInt!, $amount: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + infuseToken: { + collectionId: $collectionId + tokenId: $tokenId + amount: $amount + } + } + ) { + uuid + action state } } `, variables: { - collection_id: 3298, //Specify the collection ID - token_id: {integer: 1}, //Specify the token ID - amount: 5000000000000000000 //Specify the amount of ENJ to infuse + collectionId: 3298, + tokenId: 1, + amount: "5000000000000000000" } }, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} + headers: {'Content-Type': 'application/json','Authorization': 'Bearer YOUR_API_TOKEN'} }) .then(response => console.log(response.data)) .catch(error => console.error(error)); @@ -193,32 +245,44 @@ axios.post('https://platform.beta.enjin.io/graphql', { import requests query = ''' -mutation Infuse( - $collection_id: BigInt! - $token_id: EncodableTokenIdInput! - $amount: BigInt! -) { - Infuse(collectionId: $collection_id, tokenId: $token_id, amount: $amount) { - id - method +mutation InfuseToken($collectionId: BigInt!, $tokenId: BigInt!, $amount: BigInt!) { + CreateTransaction( + network: ENJIN + chain: MATRIX + transaction: { + infuseToken: { + collectionId: $collectionId + tokenId: $tokenId + amount: $amount + } + } + ) { + uuid + action state } } ''' variables = { - 'collection_id': 3298, #Specify the collection ID - 'token_id': {'integer': 1}, #Specify the token ID - 'amount': 5000000000000000000 #Specify the amount of ENJ to infuse + 'collectionId': 3298, + 'tokenId': 1, + 'amount': "5000000000000000000" } response = requests.post('https://platform.beta.enjin.io/graphql', json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_TOKEN'} ) print(response.json()) ``` -Note that you are only allowed to Infuse existing token if you are the collection owner, or if the token's `anyoneCanInfuse` state is set to `True`. +You can only infuse ENJ into an existing token if you are the collection owner, or if the token's `anyoneCanInfuse` state is set to `true`. + +:::tip Batched infusions +To infuse ENJ across multiple tokens in a single on-chain transaction, use the `infuseTokens` (plural) discriminator action. See the [Tokens mutations reference](/03-api-reference/02-mutations/03-tokens-mutations.md) for its input shape. +::: + +Once an infusion transaction reaches `FINALIZED`, a `MultiTokens.Infused` event is emitted with the collection ID, token ID, account, and amount infused. See [Working with Events](/05-enjin-platform/03-working-with-events.md) for how to read it. From 54b624b59fa5c6e74783b3eed0b1e8fdf395f97a Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 27 May 2026 17:26:59 +0300 Subject: [PATCH 55/67] Migrate metadata-standard, dynamic-metadata, enjin-tech-stack to v3 - Metadata Standard: convert both BatchSetAttribute examples to the batchSetTokenAttribute discriminator on CreateTransaction; drop the v2 EncodableTokenIdInput wrapper (tokenId is now a flat BigInt); switch response selection to uuid/action/state. - Dynamic Metadata: convert the BatchSetAttribute example to the batchSetCollectionAttribute discriminator (input {id, attributes}); same response shape switch. Hierarchy explanation and Pinata setup steps are unchanged. - Enjin Tech-Stack: drop Beam from the ecosystem-component list and from the crowdfunding paragraph (Beam is removed in v3). All three architecture/use-case images verified accurate (no Beam shown); kept as-is. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../02-metadata-standard.md | 74 +++++++++++-------- .../03-dynamic-metadata.md | 28 ++++--- docs/06-enjin-products/01-enjin-tech-stack.md | 4 +- 3 files changed, 62 insertions(+), 44 deletions(-) diff --git a/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md b/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md index b40d571..24e2c2b 100644 --- a/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md +++ b/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md @@ -28,50 +28,62 @@ Ensure you use the correct casing when defining attributes to avoid errors. There are two ways to set metadata for your tokens: -1. **Off-chain - Hosted JSON File:** Set the collection or token attribute with the key `uri` and the value as a URL to a off-chain hosted JSON file. This file can be hosted either in a centralized manner or on IPFS. - The JSON file should contain metadata that follows the structure defined in the metadata standard. +1. **Off-chain - Hosted JSON File:** Set the collection or token attribute with the key `uri` and the value as a URL to an off-chain hosted JSON file. This file can be hosted either in a centralized manner or on IPFS. + The JSON file should contain metadata that follows the structure defined in the metadata standard. Example: ```graphql mutation SetMetadataUsingJSON { - BatchSetAttribute( - collectionId: 36105 # Specify the collection ID - tokenId: {integer: 0 } # Specify the token ID + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + batchSetTokenAttribute: { + collectionId: 36105 # Specify the collection ID + tokenId: 0 # Specify the token ID attributes: [ - { - key: "uri" - value: "https://yourhost/metadata.json" #Link to your hosted JSON file. - } + { + key: "uri" + value: "https://yourhost/metadata.json" # Link to your hosted JSON file + } ] - ) { - id - method - state - } + } + } + ) { + uuid + action + state + } } ``` -2. **On-chain - Directly Setting Metadata:** Set the metadata directly on-chain using a structure that follows the metadata standard. +2. **On-chain - Directly Setting Metadata:** Set the metadata directly on-chain using a structure that follows the metadata standard. Example: ```graphql mutation SetMetadataDirectly { - BatchSetAttribute( - collectionId: 36105 # Specify the collection ID - tokenId: {integer: 0 } # Specify the token ID + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + batchSetTokenAttribute: { + collectionId: 36105 # Specify the collection ID + tokenId: 0 # Specify the token ID attributes: [ - { - key: "name" #This attribute key defines the token name, following the metadata standard. - value: "Chronicles of the Celestium" #Token name - }, - { - key: "description" #This attribute key defines the token description, following the metadata standard. - value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." #Token description - } + { + key: "name" # This attribute key defines the token name, following the metadata standard + value: "Chronicles of the Celestium" + }, + { + key: "description" # Token description, following the metadata standard + value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." + } ] - ) { - id - method - state - } + } + } + ) { + uuid + action + state + } } ``` diff --git a/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md b/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md index edfe142..550ec40 100644 --- a/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md +++ b/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md @@ -134,22 +134,28 @@ Finally, let's tell your collection where to find these new metadata files. You' 1. The `value` you need to set is your **``** from the last step, followed by the `/{id}.json` placeholder. * **Example Value:** `ipfs://bafy...xyz/{id}.json` (Replace `bafy...xyz` with your actual ``). -2. Set this attribute using the `BatchSetAttribute` mutation. You can copy/paste the example below into the [graphiql playground](https://platform.beta.enjin.io/graphiql), and change the values. +2. Set this attribute using the `batchSetCollectionAttribute` action on `CreateTransaction`. You can copy/paste the example below into the [GraphiQL playground](https://platform.beta.enjin.io/graphiql) and change the values. ```graphql mutation SetDynamicMetadataURI { - BatchSetAttribute( - collectionId: 8143 # 👈 Change this to your Collection ID - # We're not setting a tokenId, so this applies to the whole collection - attributes: [ - { - key: "uri" - # 👇 Change this to your JSON folder's CID + the {id} placeholder - value: "ipfs://bafy...xyz/{id}.json" + CreateTransaction( + network: ENJIN # or CANARY for testnet + chain: MATRIX + transaction: { + batchSetCollectionAttribute: { + id: 8143 # 👈 Change this to your Collection ID + attributes: [ + { + key: "uri" + # 👇 Change this to your JSON folder's CID + the {id} placeholder + value: "ipfs://bafy...xyz/{id}.json" + } + ] } - ] + } ) { - id + uuid + action state } } diff --git a/docs/06-enjin-products/01-enjin-tech-stack.md b/docs/06-enjin-products/01-enjin-tech-stack.md index 4f97b75..8ff8392 100644 --- a/docs/06-enjin-products/01-enjin-tech-stack.md +++ b/docs/06-enjin-products/01-enjin-tech-stack.md @@ -3,7 +3,7 @@ title: "Enjin Tech-Stack" slug: "enjin-tech-stack" description: "Explore the Enjin Ecosystem, providing innovative blockchain solutions for developers, enabling the integration of NFTs, tokens, and decentralized applications." --- -Enjin is the ideal blockchain platform for your game or business due to its gaming roots and the powerful Relaychain infrastructure that it's built upon. The Enjin ecosystem comprises the Enjin Platform, NFT.io, Enjin Wallet 2.0, Beam, SDKs, and other tools and services that allow game and app developers to boost their revenue, gain a competitive advantage, and innovate in ways that were previously impossible. +Enjin is the ideal blockchain platform for your game or business due to its gaming roots and the powerful Relaychain infrastructure that it's built upon. The Enjin ecosystem comprises the Enjin Platform, NFT.io, Enjin Wallet 2.0, SDKs, and other tools and services that allow game and app developers to boost their revenue, gain a competitive advantage, and innovate in ways that were previously impossible. ![](/img/components/1.webp) @@ -16,7 +16,7 @@ Unlike traditional blockchain game development that requires building and mainta

-The Enjin Ecosystem provides numerous advantages, one of which is the ability to leverage innovative crowdfunding models. With this platform, you can create gaming assets such as characters, weapons, and real estate ahead of time and offer them to gamers through conventional crowdfunding platforms or a custom website powered by NFT.io. Using Beam, the world's first QR-based blockchain asset distribution service, assets can be easily shared with QR codes. Furthermore, you can facilitate user-driven value creation by enabling players to modify, craft, upgrade, build, and trade blockchain-based gaming assets, which can gain intrinsic value based on their history, stats, utility, and customization. +The Enjin Ecosystem provides numerous advantages, one of which is the ability to leverage innovative crowdfunding models. With this platform, you can create gaming assets such as characters, weapons, and real estate ahead of time and offer them to gamers through conventional crowdfunding platforms or a custom website powered by NFT.io. You can also facilitate user-driven value creation by enabling players to modify, craft, upgrade, build, and trade blockchain-based gaming assets, which can gain intrinsic value based on their history, stats, utility, and customization. Enjin's Ecosystem provides benefits beyond just cutting-edge crowdfunding models, including the establishment of user loyalty and trust through true ownership of digital gaming assets. This ownership leads to more loyal users who are more likely to make lost revenue by eliminating gray market trading, which can account for up to 40% of lost revenue. Furthermore, player churn can be reduced by ensuring gaming assets are owned by gamers and safely stored in their private Enjin Wallet, which requires approval for each transaction, thus making hacking or in-game fraud impossible. From 658a7d00621c02f2e1c871932ee0740dc2782975 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 27 May 2026 18:03:39 +0300 Subject: [PATCH 56/67] Rewrite The Multiverse page for v3 and the seasonal quest model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop Multiverse Codex framing throughout; replace with the Multiverse Dashboard at multiverse.nft.io. - Replace the monthly-quest model (12-quest yearly schedule, item- per-month rewards, Discord-linked wallet onboarding) with the new seasonal Multiverse Quest: four seasons per year, 2026 theme "Essence of the Elements" (Fire/Water/Wind/Earth), Essence-NFT progress meter (auto-minted on wallet connect, up to 180 per game per season), end-of-season draw chances weighted by Essence. - Simplify "How to Join" to just Spark Program + reach out to Enjin (drop the "adopt 5 items including the chest + the current quest's item" requirement). - Keep the Primythical Chest as its own section but reposition it as a showcase Multiverse Item distributed via promotions, not the primary onboarding funnel (Essence now plays that role). Drop the Beam QR-scan distribution language (Beam is removed in v3). - Keep the Multiverse Items integration section. Update the broken inbound link to wallet connecting: was the deprecated 01-using-wallet-connect path, now points at sending-wallet-requests (the v3 native wallet-linking flow). reading-user-wallets link is unchanged. - Multiverse Collection table: add a Group ID column. Group IDs for the 12 grouped items were queried live via GetCollection(2967).tokenGroups against the v3 API and matched to each item by fetching the group URI's JSON (which carries the item name). The 4 single-token "stackable" items (Primythical Chest, Aeonclipse Key, Archspire, Tramyarus Quarter) have no group — shown as "—". Added a forward-pointer to the Token Groups query reference alongside the existing GetTokens pointer. - Drop all 5 outdated graphics: the hero "Unlock an ecosystem of monthly active players" banner, the Beam-QR Primythical Chest promo, the QR-to-Codex flow diagram, the dated "Next Quest" UI shot, and the Discord-flow "Multiverse Quests for monthly retention" diagram. All reference superseded systems. - Cut the heavy marketing-prose "self-sustaining cycle" 4-bullet list; replace with a single concise sentence in the intro. - Update three glossary entries to match: the_multiverse (drop Codex + monthly), multiverse_quests (mention seasonal + Essence + 2026 theme), primythical_chest (drop QR-scan specifics). multiverse_codex glossary entry left in place but is no longer referenced anywhere in the docs. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../06-the-multiverse.md | 175 +++++------------- src/glossary.js | 6 +- 2 files changed, 49 insertions(+), 132 deletions(-) diff --git a/docs/02-guides/01-platform/03-advanced-mechanics/06-the-multiverse.md b/docs/02-guides/01-platform/03-advanced-mechanics/06-the-multiverse.md index 62f9571..d7078e2 100644 --- a/docs/02-guides/01-platform/03-advanced-mechanics/06-the-multiverse.md +++ b/docs/02-guides/01-platform/03-advanced-mechanics/06-the-multiverse.md @@ -1,126 +1,43 @@ --- title: "The Multiverse" slug: "the-multiverse" -description: "Learn how to integrate the multiverse into your game or application, allowing users to access cross-game blockchain assets and experiences." +description: "Learn how to integrate Multiverse Items into your game and join the Enjin Multiverse — a cross-game ecosystem where shared NFTs power player acquisition and retention." --- import GlossaryTerm from '@site/src/components/GlossaryTerm'; -![Using the multiverse](/img/guides/managing-users/using-the-multiverse.jpg) +The **Enjin ** is a connected set of games and experiences built on a core idea: a player's gaming items can move with them. The [Multiverse NFT Collection](#the-multiverse-collection) brings these assets to life — its tokens are the shared in-game items of the Enjin ecosystem, recognized and given across every Multiverse game. - is the world's first cross-game item economy, that offers you a powerful growth engine to attract new players into your game and keep them coming back every month. +The **[Multiverse Quest](https://multiverse.nft.io/)** is Enjin's seasonal gaming campaign. Players play different Enjin games to earn **Essence**, which determines their share of that season's Multiverse Item rewards. As more games join the ecosystem, the utility of each item grows — giving players more reasons to keep playing and earning. -When you join, you will be promoted on the [Multiverse Codex](https://enjin.io/multiverse), which acts as an inbound funnel that showcases your game and gives players more reasons to try it. +## How the Multiverse Quest works -These players can then participate in , a monthly event where you can reward them with when they play your game. +The Multiverse Quest runs in four seasons per year. The 2026 theme is **Essence of the Elements** — each season features a natural element (Fire, Water, Wind, Earth) and distributes its own set of rewards. -In return, you’re asked to add to the Multiverse Items you distribute, this makes them more desirable to players as they endlessly grow in power. +### Essence -This creates an self-sustaining cycle that powers your game's growth: +Essence is the player's points-and-progress meter for the season. More Essence means better rewards and stronger draw chances at the end of the season. -1. **Ecosystem-Wide Promotion Fuels Player Attraction**: Multiverse Quests are promoted across the entire ecosystem, drawing in players who are interested in earning valuable cross-game items. This shared onboarding funnel not only boosts individual games but strengthens the whole ecosystem. +- When a player connects their gaming wallet on the [Multiverse Dashboard](https://multiverse.nft.io/), they automatically receive a free **Essence NFT**. +- Each Multiverse game can award up to **180 Essence per season** to a player. +- At the end of the season, accumulated Essence determines that player's share of the season's reward pool. -2. **Monthly Rewards Fuel Player Retention**: By offering new rewards each month, players have a continuous reason to return, keeping the community lively and excited. +For the current season's element, integrated games, and reward details, see [multiverse.nft.io](https://multiverse.nft.io/). -3. **Growing Playerbase Attracts More Developers:** As the player community expands, more developers are drawn to the ecosystem, eager to tap into this engaged audience. They adopt the Multiverse items, which in turn gives new and existing players even more reasons to continue playing the existing games to earn these ever-evolving items. +## How to join the Multiverse as a Game -4. **Utility of Multiverse Items Expands**: As more developers participate, the utility of Multiverse items increases across a wider range of games, making them even more appealing to players and attracting new players into the ecosystem. - -This cycle—driven by consistent player engagement, developer involvement, and expanding item utility—creates a powerful loop that forms a self-reinforcing growth engine. - -## Using the Primythical Chest to Acquire Players - -The is an infinite supply promotional tool and Enjin's primary funnel for onboarding new players into your game. - -Enjin is giving away this item for free, then educating players on where they can use it. - -Players can receive the item by scanning an QR code to get it for free. - -![](/img/guides/managing-users/using-the-multiverse-2.jpg) - -Once they receive the item, they are directed to the [Multiverse Codex](https://enjin.io/multiverse), where they can find games to play where their new item has utility. This is how your game will get discovered. - -Players will also start to discover a handful of other that they can earn by playing yours and other games. - -This will lead them to participate in , where they will play your game every month to earn more Multiverse Items. - -![](/img/guides/managing-users/using-the-multiverse-3.jpg) - -### How to Add Your Game - -To add your game to the [Multiverse Codex](https://enjin.io/multiverse), follow these steps: - -1. Join the [Enjin Spark Program](https://enjin.io/join). -2. Adopt at least five Multiverse Items, including the Primythical Chest. - -In return, your game will be promoted on the [Multiverse Codex](https://enjin.io/multiverse). While Enjin will help drive players to your game, it's your responsibility to convert their interest into lasting engagement. - -Your placement on each item's codex will depend on the quality of your utility and the video content you provide. - -## Using Multiverse Quests to Retain Players - -Multiverse Items are designed as powerful tokens with across multiple games. Both new and existing players are always eager to collect these items, especially as they see their utility expand with adoption by more games. - -Each month, Enjin will supply you with Multiverse Items to attract players eager to earn them in your game. - -Monthly events will feature new opportunities for players to win different Multiverse Items in your game, creating ongoing engagement between you and the Enjin playerbase. - -Over time, players may aim to collect all available items or trade them on [NFT.io](https://nft.io). - -![](/img/guides/managing-users/using-the-multiverse-4.png) - -To enter the quest, players will need to follow these steps: - -**1. Download the [Enjin Wallet](https://enjin.io/technology/wallet)** - -- Receive, secure, and manage your cross-game inventory. Available on [Android](https://play.google.com/store/apps/details?id=com.enjin.mobile.wallet&referrer=utm_source%3Dorganic_software_wallet%26utm_medium%3DOrganic%26utm_term%3Dsoftware_wallet_LP%26utm_content%3DDownload_Icon) or [iOS](https://apps.apple.com/us/app/enjin-cryptocurrency-wallet/id1349078375?ls=1). - -**2. Link Their Enjin Wallet to [Discord](https://enj.in/discord)** - -- Go to the [#link-wallet](https://enj.in/discord) channel in Enjin’s Discord server and follow the prompts. - -**3. Start Their First Multiverse Quest** - -- Find the [#multiverse-quests](https://enj.in/discord) channel in Enjin’s Discord to see a full list of games that are dropping this coveted item. - -![](/img/guides/managing-users/using-the-multiverse-5.jpg) - -### How to Add Your Game - -As new games join The Multiverse, the utility and demand for these items will continue to grow, giving players more motivation to earn them by playing your game. - -To access a supply of Multiverse Items and have Enjin promote your game as a location where players can earn these items, you must: +To list your game in the Multiverse Quest and gain access to a supply of Multiverse Items to distribute as rewards: 1. Join the [Enjin Spark Program](https://enjin.io/join). -2. Adopt at least five Multiverse Items, including the Primythical Chest. -3. Adopt the item featured in the current quest. - -While Enjin helps drive players to your game, it’s up to you to convert their interest into sustained engagement. - -Every month, Enjin will launch a new Multiverse Quest focused on a different item in the collection. - -No new items are planned for this collection, meaning that as more developers enter the ecosystem, they will only need to add utility to the existing items to participate in Multiverse Quests. This will lead to these items growing in utility over time. +2. [Contact Enjin](https://enjin.io/contact) to coordinate your integration. -To stimulate adoption of these items, Enjin will mint new supply access each month. Developers use this supply to attract and retain players. However, as long as the utility of the items and the player base in the ecosystem grow faster than the supply of Multiverse Items, the benefits for both developers and players are clear. +Enjin promotes your game across the ecosystem to drive players in — it's then your responsibility to convert their interest into lasting engagement. -Games with the highest monthly active users will receive a greater share of the reward supply. However, this metric for supply allocation may change in the future. +## The Primythical Chest -Here is the yearly release schedule for Multiverse Items & Quests: +The is an infinite-supply Multiverse Item that Enjin distributes for free as part of Multiverse promotions. It exists to showcase the cross-game utility model — a single item that players can carry into any Multiverse game and use for an in-game benefit. -| Quest | Date | Prize Pool | -| :--- | :--- | :--- | -| The Search for the Shadowsong | October 1st to October 15th | 100 x Shadowsongs | -| The Timeless Quarter of Tramyarus | November 1st to November 15th | 100 x Tramyarus Quarters | -| Mike's Misadventures | December 1st to December 15th | 500 x Mikes | -| The Nethermiser’s Vengance | January 1st to January 15th | 150 x APG-M55s | -| The Mask of the Mage-King | February 1st to February 15th | 100 x Masks of U'thuchul | -| The Blood of the Wanderer | March 1st to March 15th | 200 x Wanderer's Elixirs | -| The Pegasoid's Pursuit | April 1st to April 15th | 100 x Pegasoid Steeds | -| The Soulshift’s Embrace | May 1st to May 15th | 200 x Soulshift Armors | -| The Summoning of the Starbow | June 1st to June 15th | 100 x Starbows | -| Forging the Titan's Hammer | July 1st to July 15th | 350 x Forgehammers | -| The Epoch of the Epochrome | August 1st to August 15th | 400 x Epochrome Swords | -| The Siege of Stormwall | September 1st to September 15th | 250 x Stormwalls | +If your game adopts the Primythical Chest as a utility-bearing item, it becomes immediately accessible to every player Enjin onboards through these promotions. ## How to Add Multiverse Items into Your Game @@ -128,42 +45,42 @@ Here is the yearly release schedule for Multiverse Items & Quests: Visit [Enjin's public resource repository](https://drive.google.com/drive/folders/1ijlDn_f9PN8GsT9kQfGZR0ObrUgEL2ge) to download 3D models for Unity and Unreal Engine. -These ready-to-integrate models will streamline your multiverse integration process. +These ready-to-integrate models will streamline your Multiverse integration. -You are also welcome to design your own 3D or 2D models to better suit your game's aesthetic, provided they are recognizable to players as the Multiverse Items. +You are also welcome to design your own 3D or 2D models to better suit your game's aesthetic, provided they remain recognizable to players as the Multiverse Items. -### Step 2: Add Utility to the Multiverse Tokens. +### Step 2: Add Utility to the Multiverse Tokens -To add utility to multiverse items, your game will need to: +To add utility to Multiverse Items, your game will need to: -1. [Link player wallets to your game](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md). +1. [Link player wallets to your game](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-sending-wallet-requests.md). 2. [Read the inventory from player wallets](/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md). -3. Validate ownership of the [multiverse items](#the-multiverse-collection). -4. Provide an in-game benefit for each multiverse item. +3. Validate ownership of the [Multiverse Items](#the-multiverse-collection). +4. Provide an in-game benefit for each Multiverse Item. -Once you have completed these steps, [contact Enjin](https://enjin.io/contact) to get listed on [The Multiverse Codex](https://enjin.io/multiverse) and collaborate on Multiverse marketing initiatives aimed at driving players into your game. +Once you've completed these steps, [contact Enjin](https://enjin.io/contact) to get your game listed on the [Multiverse Dashboard](https://multiverse.nft.io/) and to collaborate on marketing initiatives that drive players into your game. ## The Multiverse Collection Collection ID: `2967` -To see if a user owns a multiverse item, use the [GetTokens query](/03-api-reference/01-queries/03-tokens-queries.md#gettokens) and filter based on Collection ID or Token IDs. - -| Token Name | Token IDs (Ranging From) | TokenIDs (Ranging To) | -| :--- | :--- | :--- | -| [Primythical Chest](https://nft.io/asset/2967-106338239662793273429419659195790131200) | 106338239662793273429419659195790131200 | N/A (/Stackable Tokens) | -| [Aeonclipse Key](https://nft.io/asset/2967-106338239662793274425543839176105918464) | 106338239662793274425543839176105918464 | N/A (/Stackable Tokens) | -| [Archspire](https://nft.io/asset/2967-106338239662793273447866403269499682816) | 106338239662793273447866403269499682816 | N/A (/Stackable Tokens) | -| [Tramyarus Quarter](https://nft.io/asset/2967-106338239662793367710728619925308440576/) | 106338239662793367710728619925308440576 | N/A (/Stackable Tokens) | -| [Epochrome Sword](https://nft.io/asset/2967-107002853660685728525072975374659354626) | 107002853660685728525072975374659354625 | 107002853660685728543519719448368906239 | -| [Forgehammer](https://nft.io/asset/2967-107002853660685728543519719448368909321) | 107002853660685728543519719448368906241 | 107002853660685728561966463522078457855 | -| [Shadowsong](https://nft.io/asset/2967-107002853660685728617306695743207113450) | 107002853660685728617306695743207112705 | 107002853660685728635753439816916664319 | -| [Oindrasdain](https://nft.io/asset/2967-107002853660685728488179487227240254752) | 107002853660685728488179487227240251393 | 107002853660685728506626231300949803007 | -| [Stormwall](https://nft.io/asset/2967-107002853660685728580413207595788010096) | 107002853660685728580413207595788009473 | 107002853660685728598859951669497561087 | -| [Soulshift Armor](https://nft.io/asset/2967-107002853660685728598859951669497561717) | 107002853660685728598859951669497561089 | 107002853660685728617306695743207112703 | -| [APG-M55](https://nft.io/asset/2967-107002853660685728561966463522078458759) | 107002853660685728561966463522078457857 | 107002853660685728580413207595788009471 | -| [Mike](https://nft.io/asset/2967-107002853660685728506626231300949809368) | 107002853660685728506626231300949803009 | 107002853660685728525072975374659354623 | -| [Starbow](https://nft.io/asset/2967-107002853660685760308813014376216789552) | 107002853660685760308813014376216788993 | 107002853660685760327259758449926340607 | -| [The Mask of U'thuchul](https://nft.io/asset/2967-107002853660685751233014930111117393999) | 107002853660685751233014930111117393921 | 107002853660685751251461674184826945535 | -| [Wanderer's Elixir](https://nft.io/asset/2967-107002853660685751251461674184826946411) | 107002853660685751251461674184826945537 | 107002853660685751269908418258536497151 | -| [Pegasoid Steed](https://nft.io/asset/2967-107002853660685760327259758449926341290) | 107002853660685760327259758449926340609 | 107002853660685760345706502523635892223 | +Multiverse Items are organized into on-chain **Token Groups** (one group per item). To check whether a user owns a given Multiverse Item, use the [GetTokens query](/03-api-reference/01-queries/03-tokens-queries.md#gettokens) and filter by Collection ID or Token IDs, or query a group's members directly via `Collection.tokenGroups` (see [Token Groups queries](/03-api-reference/01-queries/07-token-groups-queries.md)). + +| Token Name | Group ID | Token IDs (Ranging From) | Token IDs (Ranging To) | +| :--- | :--- | :--- | :--- | +| [Primythical Chest](https://nft.io/asset/2967-106338239662793273429419659195790131200) | — | 106338239662793273429419659195790131200 | N/A (/Stackable Tokens) | +| [Aeonclipse Key](https://nft.io/asset/2967-106338239662793274425543839176105918464) | — | 106338239662793274425543839176105918464 | N/A (/Stackable Tokens) | +| [Archspire](https://nft.io/asset/2967-106338239662793273447866403269499682816) | — | 106338239662793273447866403269499682816 | N/A (/Stackable Tokens) | +| [Tramyarus Quarter](https://nft.io/asset/2967-106338239662793367710728619925308440576/) | — | 106338239662793367710728619925308440576 | N/A (/Stackable Tokens) | +| [Epochrome Sword](https://nft.io/asset/2967-107002853660685728525072975374659354626) | 694 | 107002853660685728525072975374659354625 | 107002853660685728543519719448368906239 | +| [Forgehammer](https://nft.io/asset/2967-107002853660685728543519719448368909321) | 696 | 107002853660685728543519719448368906241 | 107002853660685728561966463522078457855 | +| [Shadowsong](https://nft.io/asset/2967-107002853660685728617306695743207113450) | 697 | 107002853660685728617306695743207112705 | 107002853660685728635753439816916664319 | +| [Oindrasdain](https://nft.io/asset/2967-107002853660685728488179487227240254752) | 692 | 107002853660685728488179487227240251393 | 107002853660685728506626231300949803007 | +| [Stormwall](https://nft.io/asset/2967-107002853660685728580413207595788010096) | 699 | 107002853660685728580413207595788009473 | 107002853660685728598859951669497561087 | +| [Soulshift Armor](https://nft.io/asset/2967-107002853660685728598859951669497561717) | 700 | 107002853660685728598859951669497561089 | 107002853660685728617306695743207112703 | +| [APG-M55](https://nft.io/asset/2967-107002853660685728561966463522078458759) | 702 | 107002853660685728561966463522078457857 | 107002853660685728580413207595788009471 | +| [Mike](https://nft.io/asset/2967-107002853660685728506626231300949809368) | 693 | 107002853660685728506626231300949803009 | 107002853660685728525072975374659354623 | +| [Starbow](https://nft.io/asset/2967-107002853660685760308813014376216789552) | 701 | 107002853660685760308813014376216788993 | 107002853660685760327259758449926340607 | +| [The Mask of U'thuchul](https://nft.io/asset/2967-107002853660685751233014930111117393999) | 698 | 107002853660685751233014930111117393921 | 107002853660685751251461674184826945535 | +| [Wanderer's Elixir](https://nft.io/asset/2967-107002853660685751251461674184826946411) | 703 | 107002853660685751251461674184826945537 | 107002853660685751269908418258536497151 | +| [Pegasoid Steed](https://nft.io/asset/2967-107002853660685760327259758449926341290) | 695 | 107002853660685760327259758449926340609 | 107002853660685760345706502523635892223 | diff --git a/src/glossary.js b/src/glossary.js index ceec00a..15a6798 100644 --- a/src/glossary.js +++ b/src/glossary.js @@ -64,12 +64,12 @@ const glossary = { storage_deposit: { name: "Storage Deposit", definition: "A Storage Deposit refers to a specific amount of ENJ tokens that must be deposited during various operations on the Enjin Blockchain for security and scaling purposes. For example, creating a collection requires a storage deposit of 6.25 ENJ, minting a token requires a storage deposit of 0.01 ENJ, and storing attributes, fuel tank accounts, and other values on-chain also requires a storage deposit. These deposits are not fees; they are refundable deposits that are returned to the issuer when the storage used for the operation is released. For instance, when a collection is destroyed, the collection owner will receive the storage deposit back." }, token_account_deposit: { name: "Token Account Deposit", definition: "A Storage Deposit of 0.01 ENJ is required to store a TokenAccount on-chain.\nA TokenAccount is created when an account has a token balance of 1 or more, and it is destroyed when that balance becomes zero, in which case the storage deposit is released." }, enj_infusion: { name: "ENJ Infusion", definition: "ENJ Infusion refers to Enjin Coins (ENJ) that are locked in a token on the Enjin Blockchain.\nThe token holder can retrieve the Infused ENJ by burning the token. This action is called \"Melting\"." }, - primythical_chest: { name: "Primythical Chest", definition: "The Primythical Chest is a specific type of Multiverse Item within the Enjin ecosystem, designed as a promotional tool with an infinite supply. It serves as a primary method for onboarding new players into games that are part of the multiverse. Players can mint the Primythical Chest for free, often by scanning a QR code, and use it across multiple games that have integrated this item. The Primythical Chest is intended to introduce players to the concept of cross-game utility and to drive engagement by encouraging them to explore different games where the chest and other Multiverse Items have value." }, + primythical_chest: { name: "Primythical Chest", definition: "The Primythical Chest is a specific type of Multiverse Item within the Enjin ecosystem, designed as a promotional tool with an infinite supply. Enjin distributes it for free as part of Multiverse promotions to showcase the cross-game utility model — a single item players can carry into any Multiverse game and use for an in-game benefit." }, utility: { name: "Utility", definition: "NFT utility refers to the practical benefits or functions that a Non-Fungible Token provides to its owner beyond its collectible or speculative value. This can include granting access to exclusive events or communities, offering usable items in video games, providing voting rights in decentralized organizations, unlocking premium content, enabling physical item redemption, representing fractional ownership of assets, or granting commercial usage rights for associated media. NFT utility aims to add tangible value to digital assets, potentially increasing their appeal and long-term worth by offering real-world applications or digital perks. The specific utility can vary widely depending on the project, ranging from simple access tokens to complex, multi-functional digital assets that serve various purposes within an ecosystem." }, multiverse_items: { name: "Multiverse Items", definition: "A Multiverse Item is a digital asset, typically represented as an NFT (Non-Fungible Token), that possesses utility across multiple, unrelated games or virtual environments within a broader ecosystem, often referred to as a \"multiverse.\" These items can be used, traded, or interacted with in different games or platforms, providing players with cross-game experiences and value. The concept is designed to enhance player engagement by allowing assets to transcend individual game boundaries, thereby increasing their overall utility and desirability." }, - multiverse_quests: { name: "Multiverse Quests", definition: "A Multiverse Quest is a special event or challenge within the multiverse ecosystem that involves players using or earning Multiverse Items across multiple games or platforms. These quests are designed to engage players by encouraging them to explore different games, complete specific tasks, and earn rewards, typically in the form of rare or valuable Multiverse Items. Each quest usually focuses on a particular item and provides a structured way for players to interact with various games, fostering cross-game engagement and building a cohesive experience within the multiverse." }, + multiverse_quests: { name: "Multiverse Quest", definition: "The Multiverse Quest is Enjin's seasonal gaming campaign. Players play different Enjin games to earn Essence, which determines their share of that season's Multiverse Item rewards. The Quest runs in four seasons per year — in 2026 the theme is Essence of the Elements, with each season featuring a natural element (Fire, Water, Wind, Earth) and its own set of rewards. See multiverse.nft.io for the current season." }, multiverse_codex: { name: "Multiverse Codex", definition: "(enjin.io/multiverse) The Multiverse Codex is a curated digital directory or catalog that lists and showcases various Multiverse Items, along with the games or platforms where these items can be utilized. It serves as a central hub for players to discover which games support their Multiverse Items and provides guidance on how to use these items across different environments within the multiverse. The Codex helps players maximize the utility of their items by connecting them with the available experiences, quests, and rewards across multiple games." }, - the_multiverse: { name: "The Multiverse", definition: "The Multiverse is a groundbreaking cross-game ecosystem that empowers developers with a powerful growth engine, driving player acquisition and retention. By integrating Multiverse Items into their games, developers tap into the Multiverse Codex - a robust inbound funnel that promotes their games and gives players compelling reasons to engage. Players participate in monthly Multiverse Quests, earning unique rewards that enhance their in-game experience. As more developers join, the utility and desirability of Multiverse Items grow, creating a self-sustaining cycle of engagement and expansion that fuels exponential growth across the entire Enjin Ecosystem." }, + the_multiverse: { name: "The Multiverse", definition: "The Multiverse is a cross-game ecosystem built on the idea that a player's gaming items can move with them. Multiverse Items are shared NFTs recognized across every Multiverse game, giving developers a growth engine for player acquisition and retention. As more games join, the utility and desirability of each item grows — creating a self-reinforcing cycle of engagement across the Enjin ecosystem. Players participate via the seasonal Multiverse Quest at multiverse.nft.io." }, mint: { name: "Mint", definition: "Minting refers to the process of creating new units of a MultiToken on the Enjin Blockchain." }, token_account: { name: "Token Account", definition: "A token account is stored in user account. It holds the account's MultiToken states like it's balance, freeze state, etc." }, burn: { name: "Burn", definition: "Burning a MultiToken refers to the act of destroying token units. Tokens can be burned by invoking the 'burn' extrinsic.\n\nFor tokens with the \"Collapsing Supply\" cap type, burning token units decreases the maximum supply of the token, ensuring that any burned units cannot be re-minted." }, From da29d03032cfe8f4084f53bd47ecf21759dd82a3 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 27 May 2026 18:11:20 +0300 Subject: [PATCH 57/67] Link Multiverse Collection grouped items to their nft.io group page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For the 12 grouped Multiverse Items in the Multiverse Collection table, swap the per-token-id nft.io asset link (nft.io/asset/2967-) for the group's nft.io page (nft.io/collection/the-multiverse/groups/) — the group page is a better landing target than a single token instance. The 4 single-token stackable items (Primythical Chest, Aeonclipse Key, Archspire, Tramyarus Quarter) keep their existing asset links — they have no group. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../06-the-multiverse.md | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/02-guides/01-platform/03-advanced-mechanics/06-the-multiverse.md b/docs/02-guides/01-platform/03-advanced-mechanics/06-the-multiverse.md index d7078e2..cc6e0ec 100644 --- a/docs/02-guides/01-platform/03-advanced-mechanics/06-the-multiverse.md +++ b/docs/02-guides/01-platform/03-advanced-mechanics/06-the-multiverse.md @@ -72,15 +72,15 @@ Multiverse Items are organized into on-chain **Token Groups** (one group per ite | [Aeonclipse Key](https://nft.io/asset/2967-106338239662793274425543839176105918464) | — | 106338239662793274425543839176105918464 | N/A (/Stackable Tokens) | | [Archspire](https://nft.io/asset/2967-106338239662793273447866403269499682816) | — | 106338239662793273447866403269499682816 | N/A (/Stackable Tokens) | | [Tramyarus Quarter](https://nft.io/asset/2967-106338239662793367710728619925308440576/) | — | 106338239662793367710728619925308440576 | N/A (/Stackable Tokens) | -| [Epochrome Sword](https://nft.io/asset/2967-107002853660685728525072975374659354626) | 694 | 107002853660685728525072975374659354625 | 107002853660685728543519719448368906239 | -| [Forgehammer](https://nft.io/asset/2967-107002853660685728543519719448368909321) | 696 | 107002853660685728543519719448368906241 | 107002853660685728561966463522078457855 | -| [Shadowsong](https://nft.io/asset/2967-107002853660685728617306695743207113450) | 697 | 107002853660685728617306695743207112705 | 107002853660685728635753439816916664319 | -| [Oindrasdain](https://nft.io/asset/2967-107002853660685728488179487227240254752) | 692 | 107002853660685728488179487227240251393 | 107002853660685728506626231300949803007 | -| [Stormwall](https://nft.io/asset/2967-107002853660685728580413207595788010096) | 699 | 107002853660685728580413207595788009473 | 107002853660685728598859951669497561087 | -| [Soulshift Armor](https://nft.io/asset/2967-107002853660685728598859951669497561717) | 700 | 107002853660685728598859951669497561089 | 107002853660685728617306695743207112703 | -| [APG-M55](https://nft.io/asset/2967-107002853660685728561966463522078458759) | 702 | 107002853660685728561966463522078457857 | 107002853660685728580413207595788009471 | -| [Mike](https://nft.io/asset/2967-107002853660685728506626231300949809368) | 693 | 107002853660685728506626231300949803009 | 107002853660685728525072975374659354623 | -| [Starbow](https://nft.io/asset/2967-107002853660685760308813014376216789552) | 701 | 107002853660685760308813014376216788993 | 107002853660685760327259758449926340607 | -| [The Mask of U'thuchul](https://nft.io/asset/2967-107002853660685751233014930111117393999) | 698 | 107002853660685751233014930111117393921 | 107002853660685751251461674184826945535 | -| [Wanderer's Elixir](https://nft.io/asset/2967-107002853660685751251461674184826946411) | 703 | 107002853660685751251461674184826945537 | 107002853660685751269908418258536497151 | -| [Pegasoid Steed](https://nft.io/asset/2967-107002853660685760327259758449926341290) | 695 | 107002853660685760327259758449926340609 | 107002853660685760345706502523635892223 | +| [Epochrome Sword](https://nft.io/collection/the-multiverse/groups/694) | 694 | 107002853660685728525072975374659354625 | 107002853660685728543519719448368906239 | +| [Forgehammer](https://nft.io/collection/the-multiverse/groups/696) | 696 | 107002853660685728543519719448368906241 | 107002853660685728561966463522078457855 | +| [Shadowsong](https://nft.io/collection/the-multiverse/groups/697) | 697 | 107002853660685728617306695743207112705 | 107002853660685728635753439816916664319 | +| [Oindrasdain](https://nft.io/collection/the-multiverse/groups/692) | 692 | 107002853660685728488179487227240251393 | 107002853660685728506626231300949803007 | +| [Stormwall](https://nft.io/collection/the-multiverse/groups/699) | 699 | 107002853660685728580413207595788009473 | 107002853660685728598859951669497561087 | +| [Soulshift Armor](https://nft.io/collection/the-multiverse/groups/700) | 700 | 107002853660685728598859951669497561089 | 107002853660685728617306695743207112703 | +| [APG-M55](https://nft.io/collection/the-multiverse/groups/702) | 702 | 107002853660685728561966463522078457857 | 107002853660685728580413207595788009471 | +| [Mike](https://nft.io/collection/the-multiverse/groups/693) | 693 | 107002853660685728506626231300949803009 | 107002853660685728525072975374659354623 | +| [Starbow](https://nft.io/collection/the-multiverse/groups/701) | 701 | 107002853660685760308813014376216788993 | 107002853660685760327259758449926340607 | +| [The Mask of U'thuchul](https://nft.io/collection/the-multiverse/groups/698) | 698 | 107002853660685751233014930111117393921 | 107002853660685751251461674184826945535 | +| [Wanderer's Elixir](https://nft.io/collection/the-multiverse/groups/703) | 703 | 107002853660685751251461674184826945537 | 107002853660685751269908418258536497151 | +| [Pegasoid Steed](https://nft.io/collection/the-multiverse/groups/695) | 695 | 107002853660685760327259758449926340609 | 107002853660685760345706502523635892223 | From aac1d9fdc2607b9d0f990e8708dcc1c1c9489b27 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 27 May 2026 18:28:29 +0300 Subject: [PATCH 58/67] Rewrite Beam page around the NFT.io Beam Dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the v2 'developers access Beam through the Platform API' framing — in v3, Beams are created and managed on nft.io/beam/dashboard. Replace the incorrect 'smart-contract vault' explanation with the actual flow (user signs a transaction authorising a daemon wallet to transfer tokens on their behalf). Rename 'Enjin Web Wallet' to 'Quick Wallet'. Replace the developer-API Types of Beams section (Mint on Demand / Batch Mint / Integer Range / Set Attributes on the Go) with the two distribution modes surfaced on NFT.io (One multi-use QR / Many single-use QRs) plus a brief mention of claim conditions and per-device limits. Mint on Demand isn't supported on NFT.io and was dropped. --- docs/06-enjin-products/05-beam.md | 69 +++++++++---------------------- 1 file changed, 20 insertions(+), 49 deletions(-) diff --git a/docs/06-enjin-products/05-beam.md b/docs/06-enjin-products/05-beam.md index dad3878..059c548 100644 --- a/docs/06-enjin-products/05-beam.md +++ b/docs/06-enjin-products/05-beam.md @@ -4,65 +4,36 @@ slug: "beam" description: "Discover how to use Enjin Beam to distribute blockchain assets through QR codes, making it easy to send and receive NFTs and tokens." --- -:::info Explore Our Comprehensive Beam Guide -For a detailed walkthrough of Enjin Beam's features and how to use them, check out our [in-depth guide for beams](/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md). -::: - ## What is Enjin Beam? -Enjin Beam offers an efficient way to market, promote, and distribute blockchain assets. By leveraging QR code technology, Enjin Beam allows for the easy distribution of blockchain assets on the Enjin blockchain. This feature is available on NFT.io and can be used by anyone who registers on the marketplace. - -Moreover, game developers can access Enjin Beam through the Enjin Platform API, which is designed to automate the distribution process. On the other hand, [NFT.io](https://nft.io/) is intended for users who prefer a more hands-on approach. - -### Here's how Enjin Beam works: - -1. First, blockchain assets are created on NFT.io or through the Enjin Platform API. -2. The assets are then placed in a smart contract, acting as a "vault", using the Beam interface. This generates a unique QR code. -3. Users can scan the code with the [Enjin Wallet](https://enjin.io/products/wallet) app and receive the blockchain asset directly into their wallet. -4. If the user does not have the Enjin Wallet installed, they can still interact with the Enjin Web Wallet interface to claim their blockchain assets using Enjin Beam. - -### Send Anything; Post Anywhere - -Enjin Beam offers a flexible way to send rewards that can be customized to fit your target audience and marketing goals. Whether it's gaming assets, branded collectibles, or early access, Enjin Beam lets you personalize your rewards to meet your needs. - -Beam QR codes can be shared through various visually accessible platforms, including social media networks, websites, ads, videos, flyers, presentations, and even within apps or games. This allows you to reach a broad audience and get your rewards in front of the right people. +Enjin Beam is a QR-based distribution tool for handing out blockchain assets — perfect for giveaways, event drops, marketing campaigns, and any moment where you want a player or fan to walk away with an NFT in their wallet. -**Enjin Beam enables you to:** +Beams are created and managed on the **[NFT.io Beam Dashboard](https://nft.io/beam/dashboard)**. Anyone with an NFT.io account can set up a Beam in a few clicks — no code required. -1. Simplify the creation and generation of QR codes with a few straightforward steps. -2. Distribute NFTs on a large scale via a QR code, making it easy for users to scan and claim. -3. Enhance your online presence and offer a user-friendly experience that appeals to a wide range of users, including those who are not familiar with crypto or blockchain technology. +### How Enjin Beam works -![](/img/components/8.png) +1. You hold (or are about to mint) assets on the Enjin blockchain. +2. On the [NFT.io Beam Dashboard](https://nft.io/beam/dashboard), you set up a Beam over those assets. You sign a transaction that authorises an Enjin daemon wallet to transfer the assets from your collection on your behalf when claims happen. +3. NFT.io generates a QR code (or set of QR codes) for the Beam. +4. A player scans the QR code with the [Enjin Wallet](https://enjin.io/products/wallet) app — or with **Quick Wallet** on NFT.io if they don't have the Enjin Wallet installed — and the asset is transferred straight to them. -:::tip Master Enjin Beam -Want to unlock the full potential of your NFT distributions? Our comprehensive guide to Enjin Beam offers advanced techniques, detailed setup instructions, and strategic insights for maximizing your campaign's reach and impact. -➡️ **[Read the In-Depth Guide to Enjin Beam](/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md)** -::: - -## Types of Beams - -### Mint on Demand - -One of the significant differences between Enjin Beam and its predecessors is the `MINT_ON_DEMAND` feature. This feature eliminates the need to pre-mint all tokens for distribution, which is the default way of creating beams on Enjin Blockchain. With this feature, Beam creators can define a parameter on the mutation for tokens to be minted directly to users' wallets at the exact moment they are claimed, reducing the time people wait for the tokens to arrive in their wallets. +### Send anything, post anywhere -![](/img/components/9.png) +Beam QR codes can live anywhere a player can see them — social posts, websites, ads, videos, flyers, slides, in-game overlays, even printed materials. That makes Beam a flexible way to put assets into the hands of an audience that may not even have a crypto wallet yet. -### Batch Mint & Batch Transfer +## Creating a Beam on NFT.io -Enjin Beam also introduces batch mint and batch transfer features, allowing multiple tokens to be minted and/or transferred in a single on-chain transaction. This reduces the overall system load during periods of high activity, and users can claim their tokens faster and at a significantly lower cost. With the ability to batch 150 mints into a single transaction, users can claim their tokens faster and at a lower cost. +Head to the **[NFT.io Beam Dashboard](https://nft.io/beam/dashboard)** and pick one of two distribution modes: -![](/img/components/10.png) +- **One multi-use QR** — a single QR code for the whole drop. Multiple devices can claim from it (up to whatever limits you set). +- **Many single-use QRs** — a batch of unique QR codes, each redeemable once. Useful when you want to hand out individual codes (e.g. one per event attendee). -### Integer Range Type +You can also configure: -The Integer Range type is another new feature that makes creating Beams more efficient. By using this feature, you can specify a sequential range of token IDs for distribution using a shorthand format. So instead of specifying all of the token IDs you want to distribute individually, we can now simply provide an integer range in this format: "[startId..endId]" e.g. ["1..10000"] you can also mix and match single IDs and ranges: ["1", "5..10", "12", "15", "20..32"] +- **Claim conditions** — restrict claims to wallets that hold a minimum amount of ENJ, own an asset from a specific collection, are in selected regions, or are on a whitelist. +- **Claim limit per device** — cap how many times the same device can claim. +- **Showcase on NFT.io** — list the Beam publicly on the marketplace so players can discover it. -![](/img/components/11.png) - -### Set attributes on the go! - -Developers can now set attributes to the minted tokens as they're being minted on-chain for the first time. This feature allows for unique and computed metadata to be added to tokens during the creation process. -This interaction with the mint on-demand feature is a match made in heaven as game developers will be able to create the tokens. At the same time, the beam is being generated, and they can set the metadata and mint the tokens as the demand for new tokens increases. - -![](/img/components/12.png) +:::tip +For step-by-step setup walkthroughs and advanced configuration options, see the help docs on the [NFT.io Beam Dashboard](https://nft.io/beam/dashboard). +::: From 6f98eb010fe551c012b5e99bd65dcec831a2439b Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Wed, 27 May 2026 18:39:52 +0300 Subject: [PATCH 59/67] Migrate misc v3 pages and delete obsolete create-qr-drops guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/06-enjin-products/05-beam.md: tighten copy ("You hold assets…"). - docs/04-enjin-blockchain/02-enjin-relaychain/02-enjin-relaychain.md: drop "Enjin Beam, Enjin Web Wallet" from the integration list. - docs/05-enjin-platform/02-chain-state.md: update the GetCollection example to v3 (network/chain args, id: arg, mintPolicy/transferPolicy wrappers, forceCollapsingSupply, isFrozen). Drop the Relay-style tokens connection. Response JSON verified live against collection 2000. - docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md: deleted. Built entirely on the v2 Beam API surface; Beams are now created on the NFT.io Beam Dashboard, covered by the rewritten 06-enjin-products/05-beam.md. --- .../01-managing-tokens/06-create-qr-drops.md | 995 ------------------ .../02-enjin-relaychain.md | 2 +- docs/05-enjin-platform/02-chain-state.md | 42 +- docs/06-enjin-products/05-beam.md | 2 +- 4 files changed, 19 insertions(+), 1022 deletions(-) delete mode 100644 docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md diff --git a/docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md b/docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md deleted file mode 100644 index 79480f6..0000000 --- a/docs/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md +++ /dev/null @@ -1,995 +0,0 @@ ---- -title: "Create QR Drops" -slug: "create-qr-drops" -description: "Discover how to create QR drops, allowing for simple distribution of blockchain tokens to users via scannable QR codes." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - - is a simple way to distribute digital assets like tokens and collectibles using QR codes. Users just need to scan a Beam QR code with their Enjin Wallet to receive tokens directly. It's a great tool for marketing campaigns, events, or promotions to give away items and easily bring new users into your ecosystem. - -:::info What you'll need: -- Some [ Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for and a deposit of 0.01 ENJ is required for the , for each new token holder. -You can obtain cENJ (Canary ENJ) for testing from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to mint. -::: - -**There are two ways to create an Enjin Beam:** - -1. [Using the Enjin Dashboard](#option-a-using-the-enjin-dashboard) -2. [Using the Enjin API & SDKs](#option-b-using-the-enjin-api--sdks) - -## Option A. Using the Enjin Dashboard - -1. Log in to your Enjin Platform account. -2. In the main menu, go to "**Beams**" ([https://platform.canary.enjin.io/beams/list](https://platform.canary.enjin.io/beams/list)). -3. Click the "**Create Enjin Beam**" button ([https://platform.canary.enjin.io/create/beam](https://platform.canary.enjin.io/create/beam)). - -![Creating a Beam](/img/guides/managing-tokens/create-beam.gif) - -Now, you'll fill out the details for your Beam: - -- **Beam Settings:** Configure the basic properties of your Beam. - - **Collection ID:** Select the collection that contains the tokens you want to distribute. - - **Beam Metadata:** Give your Beam a name, description, and add an image. This is what users will see when they scan the QR code. - -

- Create Beam Form -

- -- **Tokens to Distribute:** This is where you define which tokens are given out and how many. - - A single Beam is made up of one or more "Beam Claims". Each time a user scans the QR code and successfully receives a token, they are redeeming one **Beam Claim**. - - **[Token IDs](/03-api-reference/04-important-arguments.md#tokenid):** Specify the IDs of the specific tokens you want to distribute with this claim. - - **Claim Quantity:** This sets how many individual Beam Claims you want to create for each selected Token ID. - - **Token Quantity Per Claim:** This sets how many units of the token a user receives *each time* they redeem a single Beam Claim. For example, if you're giving out stacks of 5 gold coins with each claim, set this to 5. - - **Type:** Choose whether redeeming a claim will `MINT` a brand new token or `TRANSFER` an existing token from your account balance. - -

- Create Beam Form 2 -

- -:::tip Use **Flags** for advanced options and more control - - **Single-use:** This important flag changes how the QR code works. - - **Default (Single-use OFF):** You get one main QR code for the entire Beam. When a user scans it, the system randomly picks one of the available Beam Claims to give them. - - **Single-use ON:** A *unique QR code* is generated for *every single Beam Claim*. - - **Why use Single-use?** It gives you precise control over which specific claim (and thus which token) goes to which person. It's also more secure for private distributions; you can share individual QR codes with specific users without worrying about a main QR code being leaked publicly. -::: - -Once you're satisfied with the options, click on the "**Create**" button at the bottom right corner to create the request. -If a signature request is needed, approve it. - -Once the Beam is created, locate it in the "**Beams**" menu, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Claim**" button to view the QR code. - -![Claiming a Beam](/img/guides/managing-tokens/claim-beam.gif) - -:::warning Need to get all of the "Single-Use" QR codes? -Please note, viewing all of the "Single-Use" QR codes is currently not available in the Platform User Interface. -For the time being, it can be viewed programmatically only via [Enjin API & SDKs](#option-b-using-the-enjin-api--sdks) -::: - -The Enjin Beam is now ready to be shared to distribute tokens. Users can scan the QR code and instantly receive your token! -Next skillset to learn is mutating a collection / token. -Proceed to the [Freezing & Thawing](/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md) tutorial to learn more. - -## Option B. Using the Enjin API & SDKs - -:::info Make sure to use the Beam Endpoint -- Testnet: `http://platform.canary.enjin.io/graphql/beam` -- Mainnet: `http://platform.beta.enjin.io/graphql/beam` - -**Try the Beam Playground here:** -- Testnet: http://platform.canary.enjin.io/graphiql/beam -- Mainnet: http://platform.beta.enjin.io/graphiql/beam -::: - -First, create the Beam with the parameters of your choice, such as name, description, image, begin and end date & the number of assets that you would like to distribute. - -**Mutation:** - - - -```graphql -mutation CreateBeam { - CreateBeam( - name: "My Beam" #Specify beam name - description: "This is a test beam." #Specify beam description - image: "https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg" #Specify url for beam image - shows during claim process - start: "1st January 2023" #Specify start date - end: "1st January 2024" #Specify end date - collectionId: 36623 #Select the collection ID - tokens: [{ - tokenIds: ["1..10", "123"] #Select the Token IDs. Use comma for multiple tokenIds: ["123", "234"], 2 dots for range: ["123", "234..345"] - tokenQuantityPerClaim:1 #Specify the number of tokens a user can receive per claim - claimQuantity:1 #Specify the number of times a user can claim - type:MINT_ON_DEMAND #Select token distribution type - }] - ) -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation CreateBeam(\r\n $name: String!\r\n $description: String!\r\n $image: String!\r\n $start: DateTime!\r\n $end: DateTime!\r\n $collection_id: BigInt!\r\n) {\r\n CreateBeam(\r\n name: $name\r\n description: $description\r\n image: $image\r\n start: $start\r\n end: $end\r\n collectionId: $collection_id\r\n tokens: [\r\n {\r\n tokenIds: [\"1..10\", \"123\"]\r\n tokenQuantityPerClaim: 1\r\n claimQuantity: 1\r\n type: MINT_ON_DEMAND\r\n }\r\n ]\r\n )\r\n}\r\n","variables":{"name":"My Beam","description":"This is a test beam.","image":"https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg","start":"1st January 2023","end":"1st January 2024","collection_id":36623}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; -using Enjin.Platform.Sdk.Beam; - -Cl -var tokens = new List -{ - new ClaimToken() - .SetTokenIds(new List - { - new IntegerRange(1, 10), - new IntegerRange(123) - }.ToArray() - ) - .SetClaimQuantity(1) - .SetTokenQuantityPerClaim(1) - .SetType(BeamType.MintOnDemand) -}; - -// Setup the mutation -var createBeam = new CreateBeam() - .SetName("My Beam") - .SetDescription("This is a test beam.") - .SetImage("https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg") - .SetStart(new DateTime(2023, 1, 1)) - .SetEnd(new DateTime(2024, 1, 1)) - .SetCollectionId(36623) - .SetTokens(tokens.ToArray()); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendCreateBeam(createBeam); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdkBeam/BeamMutations.hpp" -#include "EnjinPlatformSdkBeam/Beam.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace enjin::platform::sdk::beam; -using namespace std; - -int main() { - - // Define the list of tokens and claim parameters to include in the Beam - vector claimTokens; - vector integerRanges; - integerRanges.emplace_back("1", "10"); - integerRanges.emplace_back("123"); - - ClaimToken claimToken1 = ClaimToken() - .SetTokenIds(make_shared>(integerRanges)) - .SetClaimQuantity(make_shared(1)) - .SetTokenQuantityPerClaim(make_shared(1)) - .SetType(BeamType::MintOnDemand); - - claimTokens.push_back(claimToken1); - - DateTime startAt = DateTime::Parse("2023-01-01T00:00:00+0"); // Note the date string should be in ISO 8601 format. - DateTime endAt = DateTime::Parse("2024-01-01T00:00:00+0"); // Note the date string should be in ISO 8601 format. - - // Setup mutation - CreateBeam createBeam = CreateBeam() - .SetName(make_shared("My Beam")) - .SetDescription(make_shared("This is a test beam.")) - .SetImage(make_shared("https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg")) - .SetStart(make_shared(startAt)) - .SetEnd(make_shared(endAt)) - .SetCollectionId(make_shared("36623")) - .SetTokens(make_shared>(claimTokens)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendCreateBeam(*client, createBeam); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& beamCode = gqlResult->GetData()->GetResult(); - - std::cout << beamCode.value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation CreateBeam - ( - $name: String! - $description: String! - $image: String! - $start: DateTime! - $end: DateTime! - $collection_id: BigInt! - ) { - CreateBeam( - name: $name - description: $description - image: $image - start: $start - end: $end - collectionId: $collection_id - tokens: [{ - tokenIds: ["1..10", "123"] #Select the Token IDs. Use comma for multiple tokenIds ["123", "234"], 2 dots for range: ["123", "234..345"] - tokenQuantityPerClaim:1 #Specify the number of tokens a user can receive per claim - claimQuantity:1 #Specify the number of times a user can claim - type:MINT_ON_DEMAND #Select token distribution type - }] - ) - } - `, - variables: { - name: "My Beam", //Specify beam name - description: "This is a test beam.", //Specify beam description - image: "https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg", //Specify url for beam image - shows during claim process - start: "1st January 2023", //Specify start date - end: "1st January 2024", //Specify end date - collection_id: 36623 //Select the collection ID - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation CreateBeam - ( - $name: String! - $description: String! - $image: String! - $start: DateTime! - $end: DateTime! - $collection_id: BigInt! - ) { - CreateBeam( - name: $name - description: $description - image: $image - start: $start - end: $end - collectionId: $collection_id - tokens: [{ - tokenIds: ["1..10" ,"123"] #Select the Token IDs. Use comma for multiple tokenIds: ["123", "234"], 2 dots for range: ["123", "234..345"] - tokenQuantityPerClaim:1 #Specify the number of tokens a user can receive per claim - claimQuantity:1 #Specify the number of times a user can claim - type:MINT_ON_DEMAND #Select token distribution type - }] - ) - } - `, - variables: { - name: "My Beam", //Specify beam name - description: "This is a test beam.", //Specify beam description - image: "https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg", //Specify url for beam image - shows during claim process - start: "1st January 2023", //Specify start date - end: "1st January 2024", //Specify end date - collection_id: 36623 //Select the collection ID - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation CreateBeam -( - $name: String! - $description: String! - $image: String! - $start: DateTime! - $end: DateTime! - $collection_id: BigInt! -) { - CreateBeam( - name: $name - description: $description - image: $image - start: $start - end: $end - collectionId: $collection_id - tokens: [{ - tokenIds: ["1..10", "123"] #Select the Token IDs. Use comma for multiple tokenIds: ["123", "234"], 2 dots for range: ["123", "234..345"] - tokenQuantityPerClaim:1 #Specify the number of tokens a user can receive per claim - claimQuantity:1 #Specify the number of times a user can claim - type:MINT_ON_DEMAND #Select token distribution type - }] - ) -} -''' - -variables = { - 'name': "My Beam", #Specify beam name - 'description': "This is a test beam.", #Specify beam description - 'image': "https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg", #Specify url for beam image - shows during claim process - 'start': "1st January 2023", #Specify start date - 'end': "1st January 2024", #Specify end date - 'collection_id': 36623 #Select the collection ID -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -Next you will need to send a query to get one of the two different code types. - -- **Multi-Use Codes:** Useful for advertising. -- **Single-Use Codes:** Useful for rewarding individuals. - -**Query: Multi-Use Codes** - - - -```graphql -query GetBeam { - GetBeam(code: "adab7ac8bda71c2e2bf8a3228f0a435f") { #Set the beam code - id - code - name - description - image - start - end - isClaimable - claimsRemaining - qr { - url - payload - } - collection { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - } - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query GetBeam($code: String!) {\r\n GetBeam(code: $code) {\r\n id\r\n code\r\n name\r\n description\r\n image\r\n start\r\n end\r\n isClaimable\r\n claimsRemaining\r\n qr {\r\n url\r\n payload\r\n }\r\n collection {\r\n collectionId\r\n maxTokenCount\r\n maxTokenSupply\r\n forceSingleMint\r\n frozen\r\n network\r\n }\r\n }\r\n}\r\n","variables":{"code":"adab7ac8bda71c2e2bf8a3228f0a435f"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; -using Enjin.Platform.Sdk.Beam; - -// Setup the query -var getBeam = new GetBeam() - .SetCode("adab7ac8bda71c2e2bf8a3228f0a435f"); - -// Define and assign the return data fragment to the query -var beamFragment = new BeamFragment() - .WithId() - .WithCode()Wi - .WithName() - .WithDescription() - .WithImage() - .WithStart() - .WithEnd() - .WithQr(new BeamQrFragment() - .WithUrl() - .WithPayload() - ) - .WithCollection(new CollectionFragment() - .WithCollectionId() - .WithMaxTokenCount() - .WithMaxTokenSupply() - .WithForceSingleMint() - .WithFrozen() - .WithNetwork() - ); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendGetBeam(getBeam); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdkBeam/BeamQueries.hpp" -#include "EnjinPlatformSdkBeam/Beam.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace enjin::platform::sdk::beam; -using namespace std; - -int main() { - - // Set up the query - GetBeam getBeam = GetBeam() - .SetCode(make_shared("adab7ac8bda71c2e2bf8a3228f0a435f")); - - // Define the fragment for the returned data - BeamFragment beamFragment = BeamFragment() - .WithId() - .WithCode() - .WithName() - .WithDescription() - .WithImage() - .WithStart() - .WithEnd() - .WithQr(make_shared( - BeamQrFragment() - .WithUrl() - .WithPayload() - ) - ) - .WithCollection(make_shared( - CollectionFragment() - .WithCollectionId() - .WithMaxTokenCount() - .WithMaxTokenSupply() - .WithForceSingleMint() - .WithFrozen() - .WithNetwork() - ) - ); - - getBeam.SetFragment(make_shared(beamFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendGetBeam(*client, getBeam); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& beam = gqlResult->GetData()->GetResult(); - - std::cout << beam->GetCode().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query GetBeam - ( - $code: String! - ) { - GetBeam(code: $code) { - id - code - name - description - image - start - end - isClaimable - claimsRemaining - qr { - url - payload - } - collection { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - } - } - } - `, - variables: { - code: "adab7ac8bda71c2e2bf8a3228f0a435f" //Set the beam code - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - query GetBeam - ( - $code: String! - ) { - GetBeam(code: $code) { - id - code - name - description - image - start - end - isClaimable - claimsRemaining - qr { - url - payload - } - collection { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - } - } - } - `, - variables: { - code: "adab7ac8bda71c2e2bf8a3228f0a435f" //Set the beam code - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -query GetBeam -( - $code: String! -) { - GetBeam(code: $code) { - id - code - name - description - image - start - end - isClaimable - claimsRemaining - qr { - url - payload - } - collection { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - } - } -} -''' - -variables = { - 'code': "adab7ac8bda71c2e2bf8a3228f0a435f" #Set the beam code -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -**Query: Single-Use Codes** - - - -```graphql -query GetSingleUseCodes { - GetSingleUseCodes(code: "adab7ac8bda71c2e2bf8a3228f0a435f") { #Set the beam code - edges { - cursor - node { - qr { - url - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query GetSingleUseCodes($code: String!) {\r\n GetSingleUseCodes(code: $code) {\r\n edges {\r\n cursor\r\n node {\r\n qr {\r\n url\r\n }\r\n }\r\n }\r\n totalCount\r\n pageInfo {\r\n hasPreviousPage\r\n hasNextPage\r\n startCursor\r\n endCursor\r\n }\r\n }\r\n}\r\n","variables":{"code":"adab7ac8bda71c2e2bf8a3228f0a435f"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; -using Enjin.Platform.Sdk.Beam; - -// Setup the query -var getSingleUseCodes = new GetSingleUseCodes() - .SetCode("adab7ac8bda71c2e2bf8a3228f0a435f"); - -// Define and assign the return data fragment to the query -var beamConnectionFragment = new ConnectionFragment() - .WithEdges(new EdgeFragment() - .WithCursor() - .WithNode(new BeamClaimFragment() - .WithQr( - new BeamQrFragment() - .WithUrl() - ) - ) - ) - .WithTotalCount() - .WithPageInfo(new PageInfoFragment() - .WithHasPreviousPage() - .WithHasNextPage() - .WithStartCursor() - .WithEndCursor() - ); - -getSingleUseCodes.Fragment(beamConnectionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendGetSingleUseCodes(getSingleUseCodes); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdkBeam/BeamQueries.hpp" -#include "EnjinPlatformSdkBeam/Beam.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace enjin::platform::sdk::beam; -using namespace std; - -int main() { - - // Set up the query - GetSingleUseCodes getSingleUseCodes = GetSingleUseCodes() - .SetCode(make_shared("adab7ac8bda71c2e2bf8a3228f0a435f")); - - // Define Fragments for the returned data - BeamClaimFragment beamClaimFragment = BeamClaimFragment(); - BeamQrFragment beamQrFragment = BeamQrFragment().WithUrl(); - beamClaimFragment.WithQr(make_shared(beamQrFragment)); - - PageInfoFragment pageInfoFragment = PageInfoFragment() - .WithHasPreviousPage() - .WithHasNextPage() - .WithHasStartCursor() - .WithHasEndCursor(); - - // Connection fragments are used for paginated data - ConnectionFragment beamConnectionFragment = ConnectionFragment() - .WithEdges( - make_shared>( - EdgeFragment() - .WithCursor() - .WithNode(make_shared(beamClaimFragment)) - ) - ) - .WithTotalCount() - .WithPageInfo(make_shared(pageInfoFragment)); - - - getSingleUseCodes.SetFragment(make_shared>(beamConnectionFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>>> futureResponse = SendGetSingleUseCodes(*client, getSingleUseCodes); - - // Get the platform response holding the HTTP data - PlatformResponsePtr>> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional>& singleUseCodes = gqlResult->GetData()->GetResult(); - - std::cout << singleUseCodes->GetTotalCount().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} - -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query GetSingleUseCodes - ( - $code: String! - ) { - GetSingleUseCodes(code: $code) { - edges { - cursor - node { - qr { - url - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } - } - `, - variables: { - code: "adab7ac8bda71c2e2bf8a3228f0a435f" //Set the beam code - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - query GetSingleUseCodes - ( - $code: String! - ) { - GetSingleUseCodes(code: $code) { - edges { - cursor - node { - qr { - url - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } - } - `, - variables: { - code: "adab7ac8bda71c2e2bf8a3228f0a435f" //Set the beam code - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -query GetSingleUseCodes -( - $code: String! -) { - GetSingleUseCodes(code: $code) { - edges { - cursor - node { - qr { - url - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } -} -''' - -variables = { - 'code': "adab7ac8bda71c2e2bf8a3228f0a435f" #Set the beam code -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -:::tip This query might return items in different pages using Connections -To learn how to use GraphQL cursors for pagination, head to [Using the API --> Pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination). -::: - -The Enjin Beam is now ready to be shared to distribute tokens. Users can scan the QR code and instantly receive your token! - -:::info Explore More Arguments -For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. - -For instance, you'll find settings such as different Beam flags types with the `Flags` argument. -::: - -:::tip What's next? -Proceed to the [Freezing & Thawing](/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md) tutorial to learn more. -::: diff --git a/docs/04-enjin-blockchain/02-enjin-relaychain/02-enjin-relaychain.md b/docs/04-enjin-blockchain/02-enjin-relaychain/02-enjin-relaychain.md index 6ab0d13..98d9b70 100644 --- a/docs/04-enjin-blockchain/02-enjin-relaychain/02-enjin-relaychain.md +++ b/docs/04-enjin-blockchain/02-enjin-relaychain/02-enjin-relaychain.md @@ -21,7 +21,7 @@ Enjin Relaychain is a decentralized and scalable network built for digital asset 3. Predictable Scalability and Costs: Enjin Blockchain takes a different approach to scalability and costs than other blockchain networks by creating an interoperable network of Matrix Chains with predictable scalability and costs. Unlike the auction-based parachain lease model used in Polkadot, Enjin's approach eliminates the hindrances and risks associated with parachain auctions and high development costs. This makes it easier and more cost-effective to launch and upgrade Matrix Chains, providing a predictable and reliable environment for developers and businesses to build on. 4. Streamlined Staking: Enjin Relaychain uses a nominated proof-of-stake mechanism on the Relaychain to maintain security. Staking is simplified through an intuitive user interface that is accessible directly from the Enjin Wallet. Users can easily allocate Enjin Coin to a curated nomination pool with just one click, removing the need for manually selecting individual nodes. The staked amount remains highly liquid and can be used within the Enjin ecosystem, without requiring it to be unbound from the nomination pool beforehand. This ensures the coin's utility and enables users to participate in securing the network seamlessly. 5. Inclusivity and Accessibility: Enjin Blockchain is founded on the principles of inclusivity and accessibility. It provides an opportunity for anyone to participate in network security and earn Enjin Coin rewards. With pre-configured packages, anyone can deploy a , Node, or Nomination Pool without the need for complex technical knowledge, making it easy for users to contribute to the network. This approach ensures that the network is open and accessible to all, promoting inclusivity and community involvement. -6. Seamless Integration with Enjin Products: Enjin Relaychain seamlessly integrates with popular Enjin products, such as the Enjin Wallet, NFT.io - NFT Marketplace, Enjin Platform, Enjin Beam, Enjin Web Wallet, Enjin Platform SDKs, and more. This provides a comprehensive and end-to-end integrated ecosystem, delivering a smooth experience for both users and developers. The integration ensures that all Enjin products leverage the benefits of the Relaychain, including scalability, low costs, and increased speed. +6. Seamless Integration with Enjin Products: Enjin Relaychain seamlessly integrates with popular Enjin products, such as the Enjin Wallet, NFT.io - NFT Marketplace, Enjin Platform, Enjin Platform SDKs, and more. This provides a comprehensive and end-to-end integrated ecosystem, delivering a smooth experience for both users and developers. The integration ensures that all Enjin products leverage the benefits of the Relaychain, including scalability, low costs, and increased speed. Enjin Relaychain is a decentralized, multi-chain network that aims to provide a superior digital asset ecosystem for NFT projects, games, brands, and communities globally. Its nominated proof-of-stake staking structure guarantees regulatory compliance and security. Enjin Relaychain offers the following key features and benefits: diff --git a/docs/05-enjin-platform/02-chain-state.md b/docs/05-enjin-platform/02-chain-state.md index e7101af..b434a45 100644 --- a/docs/05-enjin-platform/02-chain-state.md +++ b/docs/05-enjin-platform/02-chain-state.md @@ -26,23 +26,17 @@ Here's an example of a query in the GraphiQL playground, showing the state of co ```graphql query GetCollection { - GetCollection(collectionId: 2000) { + GetCollection(network: ENJIN, chain: MATRIX, id: 2000) { owner { - account { - address - } + address } - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - tokens { - totalCount - edges { - node { - tokenId - } - } + mintPolicy { + maxTokenCount + maxTokenSupply + forceCollapsingSupply + } + transferPolicy { + isFrozen } } } @@ -52,17 +46,15 @@ query GetCollection { "data": { "GetCollection": { "owner": { - "account": { - "address": "efTpCuJYg7jnjA8HxYb9dFKp7eCP7WKKHVoCjqy1Dt7NABds6" - } + "address": "efTpCuJYg7jnjA8HxYb9dFKp7eCP7WKKHVoCjqy1Dt7NABds6" + }, + "mintPolicy": { + "maxTokenCount": 0, + "maxTokenSupply": "0", + "forceCollapsingSupply": false }, - "maxTokenCount": null, - "maxTokenSupply": null, - "forceSingleMint": false, - "frozen": false, - "tokens": { - "totalCount": 0, - "edges": [] + "transferPolicy": { + "isFrozen": false } } } diff --git a/docs/06-enjin-products/05-beam.md b/docs/06-enjin-products/05-beam.md index 059c548..c0a50b2 100644 --- a/docs/06-enjin-products/05-beam.md +++ b/docs/06-enjin-products/05-beam.md @@ -12,7 +12,7 @@ Beams are created and managed on the **[NFT.io Beam Dashboard](https://nft.io/be ### How Enjin Beam works -1. You hold (or are about to mint) assets on the Enjin blockchain. +1. You hold assets on the Enjin blockchain. 2. On the [NFT.io Beam Dashboard](https://nft.io/beam/dashboard), you set up a Beam over those assets. You sign a transaction that authorises an Enjin daemon wallet to transfer the assets from your collection on your behalf when claims happen. 3. NFT.io generates a QR code (or set of QR codes) for the Beam. 4. A player scans the QR code with the [Enjin Wallet](https://enjin.io/products/wallet) app — or with **Quick Wallet** on NFT.io if they don't have the Enjin Wallet installed — and the asset is transferred straight to them. From 7f3a5e929a261d4a0d80fc80d2dd542626a31327 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Thu, 28 May 2026 17:00:05 +0300 Subject: [PATCH 60/67] Restore TokenGroup.collectionId and Listing.type in API ref examples These fields were previously dropped from the token-groups and marketplace query examples because selecting them returned an internal server error. The platform fixed both, so the examples now select them and the stale bug callouts are removed. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/03-api-reference/01-queries/06-marketplace-queries.md | 6 ++++-- docs/03-api-reference/01-queries/07-token-groups-queries.md | 6 ++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/03-api-reference/01-queries/06-marketplace-queries.md b/docs/03-api-reference/01-queries/06-marketplace-queries.md index 15fb028..e127da3 100644 --- a/docs/03-api-reference/01-queries/06-marketplace-queries.md +++ b/docs/03-api-reference/01-queries/06-marketplace-queries.md @@ -43,6 +43,7 @@ query GetListing { minTakeValue isActive highestPrice + type } } ``` @@ -60,7 +61,8 @@ query GetListing { "price": "620000000000000000000", "minTakeValue": "1209000000000000000000", "isActive": true, - "highestPrice": "620000000000000000000" + "highestPrice": "620000000000000000000", + "type": "FIXED_PRICE" } } } @@ -72,7 +74,7 @@ Field notes: - `makeAsset` is the token being sold; `takeAsset` is the asset that fills the listing. When `takeAsset` is `null`, the listing is denominated in ENJ. - `highestPrice` is `null` for fixed-price listings; for auctions it's the current top bid. -- `Listing.type` is part of the schema but currently returns an internal server error — omit it for now. The platform team is aware and a fix is planned. In the meantime, you can usually infer fixed-price vs. auction vs. offer from `highestPrice`/`takeAsset` shape. +- `type` is the listing kind — `FIXED_PRICE`, `AUCTION`, or `OFFER`. ## GetListings diff --git a/docs/03-api-reference/01-queries/07-token-groups-queries.md b/docs/03-api-reference/01-queries/07-token-groups-queries.md index cb3facd..8c256ad 100644 --- a/docs/03-api-reference/01-queries/07-token-groups-queries.md +++ b/docs/03-api-reference/01-queries/07-token-groups-queries.md @@ -32,6 +32,7 @@ query GetCollectionGroups { id tokenGroups { id + collectionId attributes { key value @@ -58,6 +59,7 @@ query GetCollectionGroups { "tokenGroups": [ { "id": "694", + "collectionId": "2967", "attributes": [ { "key": "uri", "value": "https://example.com/metadata/token-groups/epochrome-sword.json" } ], @@ -80,10 +82,6 @@ query GetCollectionGroups { `tokenGroupTokens[].tokenId` is the canonical token id (`"-"`), not the raw `tokenId` integer. -:::warning Selecting TokenGroup.collectionId returns an error -The `collectionId` field on `TokenGroup` is in the schema but currently returns an `Internal server error` when selected. Omit it — the parent `Collection.id` already gives you the collection id. The platform team is aware and a fix is planned. -::: - ## Reading the groups a token belongs to Use [`GetToken`](/03-api-reference/01-queries/03-tokens-queries.md#gettoken) and select `tokenGroupTokens` → `tokenGroup`. This is the lookup to use when you need to check whether a player's token grants in-game utility tied to a group. From b6b14bd11f1d114cceaadde72d072dc84e479cd7 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Thu, 28 May 2026 18:28:54 +0300 Subject: [PATCH 61/67] Rewrite Using Fuel Tanks for v3 around managed-wallet dispatching Lead with the recommended managed-wallet + Require Signature pattern (cover players' fees via a tank locked to your daemon wallet), document tank setup as an Enjin Blockchain Console walkthrough, and reduce dispatching to the single fuelTank argument. Drop the obsolete CreateFuelTank / Dispatch / DispatchAndTouch flows. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../02-managing-users/04-using-fuel-tanks.md | 1079 ++--------------- .../v3-console-create-fuel-tank-button.png | Bin 0 -> 99860 bytes .../v3-console-create-fuel-tank-form.png | Bin 0 -> 96626 bytes .../v3-console-network-chain-selector.png | Bin 0 -> 109200 bytes .../v3-console-network-fuel-tanks-menu.png | Bin 0 -> 29456 bytes 5 files changed, 97 insertions(+), 982 deletions(-) create mode 100644 static/img/guides/managing-users/v3-console-create-fuel-tank-button.png create mode 100644 static/img/guides/managing-users/v3-console-create-fuel-tank-form.png create mode 100644 static/img/guides/managing-users/v3-console-network-chain-selector.png create mode 100644 static/img/guides/managing-users/v3-console-network-fuel-tanks-menu.png diff --git a/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md b/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md index 7acc5d6..d90f379 100644 --- a/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md +++ b/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md @@ -1,1055 +1,170 @@ --- title: "Using Fuel Tanks" slug: "using-fuel-tanks" -description: "Discover how to use fuel tanks to cover blockchain transaction fees, creating a seamless and cost-free experience for users within your application." +description: "Use a fuel tank to pay transaction fees on your players' behalf, so they can transact without ever holding ENJ." --- import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -A Fuel tank is a pool that holds Enjin Coins (ENJ) which is used to cover transaction fees for eligible users. Each is designed to minimize transaction costs, resulting in lower expenses for platform operations. +A is an on-chain pool of (ENJ) that pays — and, optionally, s — on behalf of other accounts. With a fuel tank, your players can transact without ever holding ENJ or worrying about gas. -Fuel Tanks are flexible and dynamic, allowing customization based on specific rules and requirements. They operate based on a set of rules, making it possible to target specific actions or accounts meeting specific criteria. +Dispatching a transaction through a tank takes a single optional argument: add `fuelTank` to any `CreateTransaction` (or `CreateBatchTransaction`) and the platform routes that transaction through the tank. -When dispatching a transaction via a Fuel Tank, the following checks must pass: +A tank decides which transactions it pays for using a set of rules. When a transaction is dispatched through a tank, all of the following must hold: -- The Fuel Tank is not frozen. -- The Fuel Tank is funded with enough s. -- The dispatcher account is allowed to use the Fuel Tank. -- The Fuel Tank allows dispatching this transaction. +- The tank is not frozen. +- The tank holds enough ENJ. +- The dispatching account is allowed to use the tank. +- The tank's rules allow this particular transaction. -:::info What you'll need: -- Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin / Canary Matrixchain to pay for , and for funding the tank. -You can obtain cENJ (Canary ENJ) for testing from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. +:::info What you'll need - An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +- Some ENJ on Enjin / Canary Matrixchain to fund the tank. You can get cENJ (Canary ENJ) for testing from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. +- The [Enjin Blockchain Console](https://console.enjin.io/) — tanks are created and configured there. ::: -## Tank Rules - -Fuel Tanks offer versatile rules for various use cases, allowing customization. - -### Dispatch Rules - -#### Rule Set - -When a call is made to a fuel tank (also known as "Dispatching"), it must be made in accordance with a set of rules. These rules, known as rule sets, can include multiple individual Dispatch Rules that determine the validity of the call. A fuel tank can have multiple rule sets, each of which controls access and permissions to the fuel tank's functionality and resources. - -#### Types of Dispatch Rules: - -- **`Whitelisted Callers`**: Subsidize s dispatched from certain s. -- **`Whitelisted Collections`**: Subsidize s involving specific . -- **`Require Token`**: Subsidize s only if the user holds a specific NFT. -- **`WhitelistedPallets`**: Subsidize s involving specific . -- **`Permitted Extrinsics`**: Subsidize s involving specific s, e.g. marketplace CreateListing transactions. -- **`Permitted Calls`**: Subsidize s involving specific s and parameters. -- **`User Fuel Budget`**: Set limits on individual fuel consumption for security. -- **`Tank Fuel Budget`**: Define collective fuel usage to extend Fuel Tank lifespan. -- **`Max Fuel Burn Per Transaction`**: Control fuel consumption per transaction for predictability. -- **`Require Signature`**: Subsidize s which were signed by a specific . - -These rules enhance the Enjin Blockchain experience, sparking new products and business models. - -#### RequireAccount Parameter - -In addition to Dispatch Rules, a Rule Set may require the dispatching account to have an existing Fuel Tank User Account in order to dispatch a call. This is configured by setting the Rule Set's `requireAccount` parameter to true. - -### Account Rules - -Adding an account to the Fuel Tank User Accounts can be done in two ways: - -1. In advance with the `add_account` extrinsic. -2. When dispatching a call with the `dispatch_and_touch` extrinsic. - -In both ways, the account is added to the tank's User Accounts only if it's successfully validated by the tank's Account Rules. - -Note: if the dispatcher is not the tank owner, he may add itself to the User Accounts only if [UserAccountManagement](#user-account-management) is configured - -#### Types of Account Rules: - -- **`Whitelisted Callers`**: Only listed accounts are able to add their account to the Fuel Tank's User Accounts. -- **`Require Token`**: Only accounts that hold the specified token are able to add their account to the Fuel Tank's User Accounts. - -### User Account Management - -By default, only the Fuel Tank owner has the permission to add accounts to the Fuel Tank User Accounts. However, the Fuel Tank can be configured to allow accounts to add themselves by setting the `tankReservesAccountCreationDeposit` argument. - -- **Default Behavior**: If the `tankReservesAccountCreationDeposit` argument is not provided, only the Fuel Tank owner can add accounts to the Fuel Tank User Accounts. -- **Self-Addition without Deposit Funding**: If the `tankReservesAccountCreationDeposit` argument is set to `false`, the Fuel Tank allows accounts to add themselves to the Fuel Tank User Accounts. However, the Fuel Tank will not provide the funds required for the Tank User Account . -- **Self-Addition with Deposit Funding**: If the `tankReservesAccountCreationDeposit` argument is set to `true`, the Fuel Tank allows accounts to add themselves to the Fuel Tank User Accounts, and it also covers the necessary funds required for the Tank User Account . - -### Coverage Policy - -By default, the fuel tank will subsidize only . -To cover both and any the dispatched call may require, set the Coverage Policy to `FEES_AND_DEPOSIT` - -## Creating Fuel Tanks - -Now that we have a basic understanding of how Fuel Tanks are structured, let's go through setting up some Fuel Tanks with specific functionality, including creating rules and customizing it to fit your needs, enabling you to reduce transaction costs for your users. - -:::info User Interface -This guide will demonstrate how to create various fuel tanks with the GraphQL API. -Note that you may also use the User Interface to create Fuel Tanks. - -To create a Fuel Tank using the Platform's User Interface, navigate to "**[Fuel Tanks](https://platform.beta.enjin.io/fuel-tanks)**" in the Platform Menu. Then, click the "**[Create Fuel Tank](https://platform.beta.enjin.io/create/fuel-tank)**" button. +:::info Where tanks are created +Fuel tanks are created and configured in the [Enjin Blockchain Console](https://console.enjin.io/) for now. **Dispatching** through a tank already works from the Platform API today (see [Dispatching through a fuel tank](#dispatching)). Tank creation and management is being added to the Enjin Platform — this page will be updated when it lands. ::: -:::warning **Feature Availability Notice** -Please be aware that the following arguments are currently unavailable on the Enjin Platform: -- `PermittedExtrinsics` cannot be set via the Enjin Platform UI. -- `WhitelistedPallets` is not supported on the Enjin Platform, both on the UI and API. -- `requireAccount` is also not supported on the Enjin Platform, both on the UI and API. -We will update the documentation once these options are available. -Code snippets with these arguments are provided for illustrative purposes only. If you wish to create a fuel tank with these options now, please use the Enjin Console at [console.enjin.io](https://console.enjin.io). -::: +## Cover transaction fees for your players {#recommended-setup} -:::info GraphQL Endpoint -`https://platform.beta.enjin.io/graphql` +The most common use of a fuel tank is paying fees for players who never touch ENJ themselves. -**Try the GraphiQL Playground here:** https://platform.beta.enjin.io/graphiql -::: +Most games onboard players with [managed wallets](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md): the game creates and operates a wallet for each player, and the player can move everything to a self-custodial wallet later. Those players don't hold ENJ, so the game covers their fees — through a fuel tank that only your application can use. -### Subsidize Token Transfers For A Collection +The key is the **Require Signature** rule. It tells the tank to accept a dispatch only if the call carries a valid signature from one specific account. Point that account at your Platform's **Wallet Daemon** address, and only your Platform account can dispatch through the tank — the platform attaches the daemon signature automatically on every transaction you send. No one else can dispatch through your tank, so it can't be drained or used to subsidize unrelated apps. -The following mutation will set up a fuel tank that only subsidizes transactions that contain a `BatchTransfer` extrinsic, and only if it involves a token from the collection with ID 36,105. +In short, the tank is configured with: - - -```graphql -mutation CreateFuelTank{ - CreateFuelTank( - name: "Collection Token Transfers" #Specify the Fuel Tank name - coveragePolicy: FEES #This is set to FEES since we only want to subsidize transaction fees, and we don't need to provide storage deposits for token transfer transactions - reservesAccountCreationDeposit: null #This is set to null since we don't want to allow tank account creations - dispatchRules: [{ - permittedExtrinsics: [BatchTransfer] #This rule specifies that only batch transfers are subsidized - whitelistedCollections: [36105] #This rule ensures that only the specified collection is subsidized - }] - ){ - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.beta.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation CreateFuelTank(\r\n $name: String!\r\n $coveragePolicy: CoveragePolicy\r\n $permittedExtrinsics: [TransactionMethod!]\r\n $whitelistedCollections: [BigInt!]\r\n) {\r\n CreateFuelTank(\r\n name: $name\r\n coveragePolicy: $coveragePolicy\r\n dispatchRules: [\r\n {\r\n permittedExtrinsics: $permittedExtrinsics #This rule specifies that only batch transfers are subsidized\r\n whitelistedCollections: $whitelistedCollections #This rule ensures that only the specified collection is subsidized\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"name":"Collection Token Transfers","coveragePolicy":"FEES","permittedExtrinsics":["BatchTransfer"],"whitelistedCollections":[36105]}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; -using Enjin.Platform.Sdk.FuelTanks; +- **User account management** enabled, with the tank reserving the account-creation deposit, so players are added to the tank automatically and never pre-fund anything. +- **Coverage policy** set to **Fees and Deposit**, so the tank pays both transaction fees and any storage deposit a call needs (for example, listing a token). +- **One rule set** with **Require account** enabled and a single **Require signature** rule set to your daemon wallet address. -// Create the array of dispatch rules for the fuel tank -var dispatchRules = new List() -{ - new DispatchRuleInputType() - .SetPermittedExtrinsics(TransactionMethod.BatchTransfer) // This rule specifies that only batch transfers are subsidized - .SetWhitelistedCollections(36105) // This rule ensures that only the specified collection is subsidized -}; +Then you fund the tank with ENJ. The rest of this section walks through it. -// Setup the mutation -var createFuelTank = new CreateFuelTank() - .SetName("Collection Token Transfers") // Set the name of the fuel tank - .SetCoveragePolicy(CoveragePolicy.Fees) // This is set to FEES since we only want to subsidize transaction fees, and we don't need to provide storage deposits for token transfer transactions - .SetReservesAccountCreationDeposit(null) // This is set to null since we don't want to allow tank account creations - .SetDispatchRules(dispatchRules.ToArray()); +### 1. Open the Fuel Tanks section {#open-fuel-tanks} -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); +Head to the [Enjin Blockchain Console](https://console.enjin.io/) and make sure the network/chain selector in the top-left corner is set to the chain you want (for testing, use Canary Matrixchain). -createFuelTank.Fragment(transactionFragment); +![The network and chain selector in the top-left of the Enjin Blockchain Console](/img/guides/managing-users/v3-console-network-chain-selector.png) -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.beta.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); +In the top bar, open **Network → Fuel tanks**. -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendCreateFuelTank(createFuelTank); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -// Coming Soon! -``` - - -```javascript -fetch('https://platform.beta.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation CreateFuelTank - ( - $name: String! - $coveragePolicy: CoveragePolicy - $permittedExtrinsics: [TransactionMethod!] - $whitelistedCollections: [BigInt!] - ) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - dispatchRules: [{ - permittedExtrinsics: $permittedExtrinsics #This rule specifies that only batch transfers are subsidized - whitelistedCollections: $whitelistedCollections #This rule ensures that only the specified collection is subsidized - }] - ){ - id - method - state - } - } - `, - variables: { - name: "Collection Token Transfers", //Specify the Fuel Tank name - coveragePolicy: "FEES", //This is set to FEES since we only want to subsidize transaction fees, and we don't need to provide storage deposits for token transfer transactions - permittedExtrinsics: ["BatchTransfer"], //This rule specifies that only batch transfers are subsidized - whitelistedCollections: [36105] //This rule ensures that only the specified collection is subsidized - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); +![The Network menu open, with Fuel tanks highlighted](/img/guides/managing-users/v3-console-network-fuel-tanks-menu.png) -axios.post('https://platform.beta.enjin.io/graphql', { - query: ` - mutation CreateFuelTank - ( - $name: String! - $coveragePolicy: CoveragePolicy - $permittedExtrinsics: [TransactionMethod!] - $whitelistedCollections: [BigInt!] - ) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - dispatchRules: [{ - permittedExtrinsics: $permittedExtrinsics #This rule specifies that only batch transfers are subsidized - whitelistedCollections: $whitelistedCollections #This rule ensures that only the specified collection is subsidized - }] - ){ - id - method - state - } - } - `, - variables: { - name: "Collection Token Transfers", //Specify the Fuel Tank name - coveragePolicy: "FEES", //This is set to FEES since we only want to subsidize transaction fees, and we don't need to provide storage deposits for token transfer transactions - permittedExtrinsics: ["BatchTransfer"], //This rule specifies that only batch transfers are subsidized - whitelistedCollections: [36105] //This rule ensures that only the specified collection is subsidized - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation CreateFuelTank - ( - $name: String! - $coveragePolicy: CoveragePolicy - $permittedExtrinsics: [TransactionMethod!] - $whitelistedCollections: [BigInt!] - ) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - dispatchRules: [{ - permittedExtrinsics: $permittedExtrinsics #This rule specifies that only batch transfers are subsidized - whitelistedCollections: $whitelistedCollections #This rule ensures that only the specified collection is subsidized - }] - ){ - id - method - state - } - } -''' - -variables = { - 'name': "Collection Token Transfers", #Specify the Fuel Tank name - 'coveragePolicy': "FEES", #This is set to FEES since we only want to subsidize transaction fees, and we don't need to provide storage deposits for token transfer transactions - 'permittedExtrinsics': ["BatchTransfer"], #This rule specifies that only batch transfers are subsidized - 'whitelistedCollections': [36105] #This rule ensures that only the specified collection is subsidized -} - -response = requests.post('https://platform.beta.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -### Subsidize Any Transaction Involving s From A Certain Collection +### 2. Create and configure the tank {#create-tank} -:::warning Upcoming Feature Notice: `WhitelistedPallets` Option -The `WhitelistedPallets` argument is not yet supported on the Enjin Platform. -The following code snippet is provided for illustrative purposes only. -If you wish to create a fuel tank with this option now, please use the Enjin Console at [console.enjin.io](https://console.enjin.io). -We will update the documentation once this option is available. -::: - -The following mutation will set up a fuel tank that only subsidizes transactions that involves any token from the collection with ID 36,105. To achieve this, we are using the same dispatch rule from the previous example (`whitelistedCollections`) and the `WhitelistedPallets` rule, allowing only extrinsics from the `MultiTokens` pallet (for actions such as `send`, `mint`, `transfer`, etc.) and `Marketplace` pallet (for actions such as `list`, `buy`, `offer`, etc. ). - - - -```graphql -mutation CreateFuelTank{ - CreateFuelTank( - name: "Collection Token Actions" #Specify the Fuel Tank name - coveragePolicy: FEES_AND_DEPOSIT #This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). - reservesAccountCreationDeposit: true #This is set to true since we want to allow tank account creations, and we want the tank to subsidize the ENJ required for the required for the Tank User Account storage deposit - dispatchRules: [{ - WhitelistedPallets: [MultiTokens, Marketplace] #This rule specifies that only extrinsics from the MultiTokens and Marketplace pallets are subsidized - whitelistedCollections: [36105] #This rule ensures that only the specified collection is subsidized - }] - ){ - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.beta.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation CreateFuelTank(\r\n $name: String!\r\n $coveragePolicy: CoveragePolicy\r\n $reserveAccountCreationDeposit: Boolean\r\n $whitelistedPallets: [String!]\r\n $whitelistedCollections: [BigInt!]\r\n) {\r\n CreateFuelTank(\r\n name: $name\r\n coveragePolicy: $coveragePolicy\r\n reservesAccountCreationDeposit: $reserveAccountCreationDeposit\r\n dispatchRules: [\r\n {\r\n whitelistedPallets: $whitelistedPallets\r\n whitelistedCollections: $whitelistedCollections\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"name":"Collection Token Actions","coveragePolicy":"FEES_AND_DEPOSIT","reserveAccountCreationDeposit":true,"whitelistedPallets":["MultiTokens","Marketplace"],"whitelistedCollections":[36105]}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; -using Enjin.Platform.Sdk.FuelTanks; +On the Fuel tanks page, click **Create fuel tank** in the top-right. -// Create the array of dispatch rules for the fuel tank -var dispatchRules = new List() -{ - new DispatchRuleInputType() - .SetWhitelistedPallets(Pallets.MultiTokens, Pallets.Marketplace) // Note: This is not yet implemented in the Platform API. This rule specifies that only extrinsics from the MultiTokens and Marketplace pallets are subsidized. - .SetWhitelistedCollections(36105) // This rule ensures that only the specified collection is subsidized. -}; +![The Fuel tanks overview page with the Create fuel tank button](/img/guides/managing-users/v3-console-create-fuel-tank-button.png) -// Setup the mutation -var createFuelTank = new CreateFuelTank() - .SetName("Collection Token Actions") // Set the name of the fuel tank - .SetCoveragePolicy(CoveragePolicy.FeesAndDeposit) // This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). - .SetReservesAccountCreationDeposit(true) // This is set to true since we want to allow tank account creations, and we want the tank to subsidize the ENJ required for the required for the Tank User Account storage deposit. - .SetDispatchRules(dispatchRules.ToArray()); +Fill out the form: -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); +- **Using the selected address** — the account that owns and funds the tank. Switch it with the account selector if needed. +- **Name** — a unique name for the tank. +- **User account management** — turn the **include option** on, and have the tank reserve the account-creation deposit so players are added automatically without paying anything up front. +- **Coverage policy** — select **Fees and Deposit**. +- **Account rules** — leave empty for this setup. +- **Dispatch rules** — click **Add rule set**, enable **Require account**, then add a **Require signature** rule and set its account to your **Wallet Daemon** address. -createFuelTank.Fragment(transactionFragment); +![The Create fuel tank form, showing the name, user account management, coverage policy, account rules, and dispatch rules fields](/img/guides/managing-users/v3-console-create-fuel-tank-form.png) -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.beta.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendCreateFuelTank(createFuelTank); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -// Coming Soon! -``` - - -```javascript -fetch('https://platform.beta.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation CreateFuelTank( - $name: String! - $coveragePolicy: CoveragePolicy - $reserveAccountCreationDeposit: Boolean - $whitelistedPallets: [String!] - $whitelistedCollections: [BigInt!] - ) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - reservesAccountCreationDeposit: $reserveAccountCreationDeposit - dispatchRules: [ - { - whitelistedPallets: $whitelistedPallets - whitelistedCollections: $whitelistedCollections - } - ] - ) { - id - method - state - } - } - `, - variables: { - name: "Collection Token Actions", //Specify the Fuel Tank name - coveragePolicy: "FEES_AND_DEPOSIT", //This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). - reserveAccountCreationDeposit: true, //This is set to true since we want to allow tank account creations, and we want the tank to subsidize the ENJ required for therequired for the Tank User Account storage deposit - whitelistedPallets: ["MultiTokens", "Marketplace"], //This rule specifies that only extrinsics from the MultiTokens and Marketplace pallets are subsidized - whitelistedCollections: [36105] //This rule ensures that only the specified collection is subsidized - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.beta.enjin.io/graphql', { - query: ` - mutation CreateFuelTank( - $name: String! - $coveragePolicy: CoveragePolicy - $reserveAccountCreationDeposit: Boolean - $whitelistedPallets: [String!] - $whitelistedCollections: [BigInt!] - ) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - reservesAccountCreationDeposit: $reserveAccountCreationDeposit - dispatchRules: [ - { - whitelistedPallets: $whitelistedPallets - whitelistedCollections: $whitelistedCollections - } - ] - ) { - id - method - state - } - } - `, - variables: { - name: "Collection Token Actions", //Specify the Fuel Tank name - coveragePolicy: "FEES_AND_DEPOSIT", //This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). - reserveAccountCreationDeposit: true, //This is set to true since we want to allow tank account creations, and we want the tank to subsidize the ENJ required for therequired for the Tank User Account storage deposit - whitelistedPallets: ["MultiTokens", "Marketplace"], //This rule specifies that only extrinsics from the MultiTokens and Marketplace pallets are subsidized - whitelistedCollections: [36105] //This rule ensures that only the specified collection is subsidized - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation CreateFuelTank( - $name: String! - $coveragePolicy: CoveragePolicy - $reserveAccountCreationDeposit: Boolean - $whitelistedPallets: [String!] - $whitelistedCollections: [BigInt!] -) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - reservesAccountCreationDeposit: $reserveAccountCreationDeposit - dispatchRules: [ - { - whitelistedPallets: $whitelistedPallets - whitelistedCollections: $whitelistedCollections - } - ] - ) { - id - method - state - } -} -''' - -variables = { - 'name': "Collection Token Actions", #Specify the Fuel Tank name - 'coveragePolicy': "FEES_AND_DEPOSIT", #This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). - 'reserveAccountCreationDeposit': True, #This is set to true since we want to allow tank account creations, and we want the tank to subsidize the ENJ required for therequired for the Tank User Account storage deposit - 'whitelistedPallets': ["MultiTokens", "Marketplace"], #This rule specifies that only extrinsics from the MultiTokens and Marketplace pallets are subsidized - 'whitelistedCollections': [36105] #This rule ensures that only the specified collection is subsidized -} +Click **Create fuel tank** and sign the transaction. Once it is on-chain, the new tank appears in the Fuel tanks list with its own address. -response = requests.post('https://platform.beta.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -### Subsidize All Transactions For Whitelisted Accounts With Budget Limitations - -The following mutation will set up a fuel tank that subsidizes any transaction, the tank is allowed to subsidize up to 5 ENJ per 30 days for each User Account in the tank, which can be created only if the account is within the whitelisted callers list. - -:::warning Upcoming Feature Notice: `requireAccount` Option -The `requireAccount` argument is not yet supported on the Enjin Platform. -The following code snippet is provided for illustrative purposes only. -If you wish to create a fuel tank with this option now, please use the Enjin Console at [console.enjin.io](https://console.enjin.io). -We will update the documentation once this option is available. +:::tip Finding your daemon wallet address +The Require Signature rule must point at the address your Platform [Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) signs with — that's what lets the platform authorize dispatches through this tank automatically. The daemon prints its address on startup and it is the address that signs your transactions by default. ::: - - -```graphql -mutation CreateFuelTank{ - CreateFuelTank( - name: "Only Specific Accounts Allowed" #Specify the Fuel Tank name - coveragePolicy: FEES_AND_DEPOSIT #This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it. - accountRules: { - whitelistedCallers: [ #This will validate that the caller is whitelisted at the time of adding the caller's account to the tank's User Accounts. - "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", - "cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP" - ] - } - dispatchRules: [{ - requireAccount: true #This is set to true since we want the tank to allow dispatching only if the caller has a User Account in the tank. - userFuelBudget: { #In here we configure how much ENJ to subsidize for each User Account. - amount: 5000000000000000000 #This will allow the tank to subsidize up to 5 ENJ from the Tank's pool per period for each User Account. - resetPeriod: 432000 #This sets the period to 432,000 blocks, which is 30 days on average with 6 seconds average block time. Meaning the tank will be able to subsidize up to 5 ENJ per 30 days for each User Account. - } - }] - ){ - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.beta.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation CreateFuelTank(\r\n $name: String!\r\n $coveragePolicy: CoveragePolicy\r\n $whitelistedCallers: [String!]\r\n $requireAccount: Boolean\r\n $userFuelBudget: FuelBudgetInputType\r\n) {\r\n CreateFuelTank(\r\n name: $name\r\n coveragePolicy: $coveragePolicy\r\n accountRules: { whitelistedCallers: $whitelistedCallers }\r\n dispatchRules: [\r\n { requireAccount: $requireAccount, userFuelBudget: $userFuelBudget }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"name":"Only Specific Accounts Allowed","coveragePolicy":"FEES","whitelistedCallers":["cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu","cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP"],"requireAccount":true,"userFuelBudget":{"amount":5000000000000000000,"resetPeriod":432000}}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; -using Enjin.Platform.Sdk.FuelTanks; - -// Create an array of whitelisted callers and assign to accountRules.whitelistedCallers -var whitelistedCallers = new List() -{ - "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", - "cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP" -}; -var accountRules = new AccountRuleInputType() - .SetWhitelistedCallers(whitelistedCallers.ToArray()); - -// Create the user fuel budget input type -var userFuelBudget = new FuelBudgetInputType() - .SetAmount(5000000000000000000) // Set the amount of ENJ that the fuel tank will provide to users. - .SetResetPeriod(432000); // Set the reset period to 432000 blocks (approximately 30 days). +### 3. Fund the tank {#fund-tank} -// Create the array of dispatch rules for the fuel tank -var dispatchRules = new List() -{ - new DispatchRuleInputType() - .SetRequireAccount(true) // Note: This property has yet to be implemented on the platform. This is set to true since we want the tank to allow dispatching only if the caller has a User Account in the tank. - .SetUserFuelBudget(userFuelBudget) // Set the user fuel budget for the dispatch rule. -}; +A tank can only pay fees while it holds ENJ. Send ENJ to the tank's address (shown in the Fuel tanks list). On Canary, top it up with cENJ from the [faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet). The list's **funds** column shows the current balance. -// Set up the mutation -var createFuelTank = new CreateFuelTank() - .SetName("Only Specific Accounts Allowed") // Set the name of the fuel tank. - .SetCoveragePolicy(CoveragePolicy.FeesAndDeposit) // This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). - .SetAccountRules(accountRules) // Set the account rules for the fuel tank. - .SetDispatchRules(dispatchRules.ToArray()); +### 4. Dispatch transactions through the tank {#dispatch-through-tank} -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); +Now any action you perform on a player's behalf can be paid for by the tank. Sign the transaction as the player's managed wallet with `signerExternalId`, and add `fuelTank` set to your tank's address: -createFuelTank.Fragment(transactionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.beta.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendCreateFuelTank(createFuelTank); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -// Coming Soon! -``` - - -```javascript -fetch('https://platform.beta.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation CreateFuelTank( - $name: String! - $coveragePolicy: CoveragePolicy - $whitelistedCallers: [String!] - $requireAccount: Boolean - $userFuelBudget: FuelBudgetInputType - ) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - accountRules: { whitelistedCallers: $whitelistedCallers } - dispatchRules: [ - { requireAccount: $requireAccount, userFuelBudget: $userFuelBudget } - ] - ) { - id - method - state - } - } - `, - variables: { - name: "Only Specific Accounts Allowed", //Specify the Fuel Tank name - coveragePolicy: "FEES", //This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it. - whitelistedCallers: [ //This will validate that the caller is whitelisted at the time of adding the caller's account to the tank's User Accounts. - "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", - "cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP" - ], - requireAccount: true, //This is set to true since we want the tank to allow dispatching only if the caller has a User Account in the tank. - userFuelBudget: { //In here we configure how much ENJ to subsidize for each User Account. - amount: 5000000000000000000, //This will allow the tank to subsidize up to 5 ENJ from the Tank's pool per period for each User Account. - resetPeriod: 432000 //This sets the period to 432,000 blocks, which is 30 days on average with 6 seconds average block time. Meaning the tank will be able to subsidize up to 5 ENJ per 30 days for each User Account. - } - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.beta.enjin.io/graphql', { - query: ` - mutation CreateFuelTank( - $name: String! - $coveragePolicy: CoveragePolicy - $whitelistedCallers: [String!] - $requireAccount: Boolean - $userFuelBudget: FuelBudgetInputType - ) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - accountRules: { whitelistedCallers: $whitelistedCallers } - dispatchRules: [ - { requireAccount: $requireAccount, userFuelBudget: $userFuelBudget } - ] - ) { - id - method - state +```graphql +mutation DispatchThroughTank { + CreateTransaction( + network: CANARY + chain: MATRIX + signerExternalId: "docs-example-player" #The managed wallet performing the action + fuelTank: "INSERT_FUEL_TANK_ADDRESS" #Your fuel tank's address, from the Fuel tanks list + transaction: { + transferToken: { + recipient: "cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ" #The recipient of the transfer + collectionId: 36105 #Specify the collection ID + tokenId: 1 #Token ID to transfer + amount: 1 #Amount to transfer } } - `, - variables: { - name: "Only Specific Accounts Allowed", //Specify the Fuel Tank name - coveragePolicy: "FEES", //This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it. - whitelistedCallers: [ //This will validate that the caller is whitelisted at the time of adding the caller's account to the tank's User Accounts. - "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", - "cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP" - ], - requireAccount: true, //This is set to true since we want the tank to allow dispatching only if the caller has a User Account in the tank. - userFuelBudget: { //In here we configure how much ENJ to subsidize for each User Account. - amount: 5000000000000000000, //This will allow the tank to subsidize up to 5 ENJ from the Tank's pool per period for each User Account. - resetPeriod: 432000 //This sets the period to 432,000 blocks, which is 30 days on average with 6 seconds average block time. Meaning the tank will be able to subsidize up to 5 ENJ per 30 days for each User Account. - } - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation CreateFuelTank( - $name: String! - $coveragePolicy: CoveragePolicy - $whitelistedCallers: [String!] - $requireAccount: Boolean - $userFuelBudget: FuelBudgetInputType -) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - accountRules: { whitelistedCallers: $whitelistedCallers } - dispatchRules: [ - { requireAccount: $requireAccount, userFuelBudget: $userFuelBudget } - ] ) { - id - method + uuid + action state } } -''' - -variables = { - 'name': "Only Specific Accounts Allowed", #Specify the Fuel Tank name - 'coveragePolicy': "FEES", #This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it. - 'whitelistedCallers': [ #This will validate that the caller is whitelisted at the time of adding the caller's account to the tank's User Accounts. - "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", - "cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP" - ], - 'requireAccount': True, #This is set to true since we want the tank to allow dispatching only if the caller has a User Account in the tank. - 'userFuelBudget': { #In here we configure how much ENJ to subsidize for each User Account. - 'amount': 5000000000000000000, #This will allow the tank to subsidize up to 5 ENJ from the Tank's pool per period for each User Account. - 'resetPeriod': 432000 #This sets the period to 432,000 blocks, which is 30 days on average with 6 seconds average block time. Meaning the tank will be able to subsidize up to 5 ENJ per 30 days for each User Account. - } -} - -response = requests.post('https://platform.beta.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) ``` - - - -A WebSocket event will also be fired so you can pick up the Fuel Tank creation in real time by listening to the app channel on the WebSocket. - -:::info Explore More Arguments -For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -::: - -## Dispatching a Call Using a Fuel Tank - -To broadcast a transaction call using a fuel tank, use the `Dispatch`/`DispatchAndTouch` mutation. - -:::tip The Fuel Tank requires a UserAccount to dispatch? -Use the `DispatchAndTouch` mutation to create a `UserAccount` and `Dispatch` at the same time in a single transaction. -::: -:::tip Not sure which Fuel Tank to select? -If you need help figuring out the best fuel tank to use for a transaction, check out this page: [Selecting a fuel tank to dispatch with](/04-enjin-blockchain/03-enjin-matrixchain/02-fuel-tank-pallet.md#selecting-a-fuel-tank-to-dispatch-with). -::: +The player's managed wallet signs the action, and the tank covers the fee and any storage deposit. Because the tank's Require Signature rule points at your daemon wallet, the platform attaches the required signature for you — there is nothing extra to sign. -### Step #1: Prepare The Mutation +Once the transaction reaches `FINALIZED`, the chain emits the usual events for the action (here, `MultiTokens.Transferred`). See [Working with Events](/05-enjin-platform/03-working-with-events.md). -First, prepare the mutation you wish to dispatch, with the `id` and `encodedData` fields in the response. -In this example, we'll dispatch a call to send a from one account to another: +## Dispatching through a fuel tank {#dispatching} -:::danger Ask for the `id` and `encodedData` fields! -Make sure to add the `id` and `encodedData` fields in the response, or the mutation will fail with an error. -::: +`fuelTank` is available on both `CreateTransaction` and `CreateBatchTransaction`. Set it to a tank's address and the platform dispatches the transaction through that tank instead of paying the fee from the signing account. The transaction runs exactly as it normally would — the only difference is who pays for it. -```graphql -mutation TransferNFT{ - SimpleTransferToken( - collectionId: 3298 - recipient: "efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr" - params: {tokenId: {integer: 1} amount: 1} - ){ - id - encodedData - } -} -``` +A dispatch succeeds only if the signing account is allowed by the tank's rules and the tank can cover the cost; otherwise the transaction fails. If you need help choosing the right tank for a given call, see [Selecting a fuel tank to dispatch with](/04-enjin-blockchain/03-enjin-matrixchain/02-fuel-tank-pallet.md#selecting-a-fuel-tank-to-dispatch-with). -### Step #2: Prepare The Dispatch Mutation +## How fuel tanks work {#how-fuel-tanks-work} -Next, you need to convert the mutation call into a String. Remove all new lines and escape double quotation marks: +A tank decides what to pay for, and for whom, through two kinds of rules: **dispatch rules**, evaluated on every call, and **account rules**, evaluated when an account is added to the tank. Both are configured on the create-tank form in the Console. -``` -mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}} -``` +### Dispatch rules and rule sets -To escape quotation marks you can use online tools such as https://tools.knowledgewalls.com/online-escape-single-or-double-quotes-from-string. +A tank can have one or more **rule sets**, each a group of individual dispatch rules. A dispatched call must satisfy a rule set to be paid for. Each rule set has its own ID, so you can grant different callers different access by giving them different rule sets. -### Step #3: Send the Dispatch Call +Available dispatch rules: -Insert the mutation String from Step #2 into a Dispatch mutation call "query" parameter and send it: +- **`Whitelisted Callers`**: Subsidize s dispatched from specific s. +- **`Whitelisted Collections`**: Subsidize s involving a specific . +- **`Require Token`**: Subsidize s only if the caller holds a specific NFT. +- **`Whitelisted Pallets`**: Subsidize s involving a specific . +- **`Permitted Extrinsics`**: Subsidize s involving specific s (for example, marketplace listing calls). +- **`Permitted Calls`**: Subsidize s involving specific s and parameters. +- **`User Fuel Budget`**: Cap how much fuel each user account can spend in a period. +- **`Tank Fuel Budget`**: Cap the tank's total fuel spend over a period to extend its lifespan. +- **`Max Fuel Burn Per Transaction`**: Cap fuel spent on any single transaction. +- **`Require Signature`**: Subsidize only s carrying a valid signature from a specific . This is the rule behind the [recommended setup](#recommended-setup) above. - - -```graphql -mutation Dispatch { - Dispatch( - tankId: "efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou" #Specify the Fuel Tank ID to dispatch with - ruleSetId: 0 #Specify the ruleset to dispatch with - dispatch: { - call: MULTI_TOKENS #Specify the pallet to use for the transaction - query: "mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}" #Insert the mutation to dispatch. - variables: null #If the mutation has variables, insert them here. - } - ){ - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.beta.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation Dispatch(\r\n $tankId: String!\r\n $ruleSetId: BigInt!\r\n $call: DispatchCall!\r\n $query: String!\r\n $variables: Object\r\n) {\r\n Dispatch(\r\n tankId: $tankId\r\n ruleSetId: $ruleSetId\r\n dispatch: { call: $call, query: $query, variables: $variables }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"tankId":"efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou","ruleSetId":0,"call":"MULTI_TOKENS","query":"mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}","variables":null}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; -using Enjin.Platform.Sdk.FuelTanks; +### Require Account -// Create an array of whitelisted callers and assign to accountRules.whitelistedCallers -var dispatchInput = new DispatchInputType() - .SetCall(DispatchCall.MultiTokens) - .SetQuery("mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \\\"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\\\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}") // Insert the mutation to dispatch. - .SetVariables(null); +A rule set can also require that the dispatching account already has a user account in the tank before it will pay for a call, by enabling **Require account** on the rule set. -// Set up the mutation -var dispatch = new Dispatch() - .SetTankId("efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou") - .SetRuleSetId(0) - .SetDispatch(dispatchInput); +### Account rules -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); +Account rules decide who is allowed to be added to the tank's user accounts. Two are available: -dispatch.Fragment(transactionFragment); +- **`Whitelisted Callers`**: Only listed accounts may be added to the tank's user accounts. +- **`Require Token`**: Only accounts that hold the specified token may be added. -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.beta.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); +An account is added either ahead of time by the tank owner, or automatically the first time it dispatches — the latter requires user account management to be enabled. -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendDispatch(dispatch); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -// Coming Soon! -``` - - -```javascript -fetch('https://platform.beta.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation Dispatch( - $tankId: String! - $ruleSetId: BigInt! - $call: DispatchCall! - $query: String! - $variables: Object - ) { - Dispatch( - tankId: $tankId - ruleSetId: $ruleSetId - dispatch: { call: $call, query: $query, variables: $variables } - ) { - id - method - state - } - } - `, - variables: { - tankId: "efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou", //Specify the Fuel Tank ID to dispatch with - ruleSetId: 0, //Specify the ruleset to dispatch with - call: "MULTI_TOKENS", //Specify the pallet to use for the transaction - query: "mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}", //Insert the mutation to dispatch. - variables: null //If the mutation has variables, insert them here as an object. - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); +### User account management -axios.post('https://platform.beta.enjin.io/graphql', { - query: ` - mutation Dispatch( - $tankId: String! - $ruleSetId: BigInt! - $call: DispatchCall! - $query: String! - $variables: Object - ) { - Dispatch( - tankId: $tankId - ruleSetId: $ruleSetId - dispatch: { call: $call, query: $query, variables: $variables } - ) { - id - method - state - } - } - `, - variables: { - tankId: "efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou", //Specify the Fuel Tank ID to dispatch with - ruleSetId: 0, //Specify the ruleset to dispatch with - call: "MULTI_TOKENS", //Specify the pallet to use for the transaction - query: "mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}", //Insert the mutation to dispatch. - variables: null //If the mutation has variables, insert them here as an object. - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests +By default, only the tank owner can add accounts to the tank's user accounts. Enabling **user account management** lets accounts be added automatically. You also choose whether the tank covers each new user account's : -query = ''' -mutation Dispatch( - $tankId: String! - $ruleSetId: BigInt! - $call: DispatchCall! - $query: String! - $variables: Object -) { - Dispatch( - tankId: $tankId - ruleSetId: $ruleSetId - dispatch: { call: $call, query: $query, variables: $variables } - ) { - id - method - state - } -} -''' +- **Tank reserves the deposit** — accounts are added without pre-funding anything. This is what the [recommended setup](#recommended-setup) uses, so players never need ENJ. +- **Tank does not reserve the deposit** — accounts can add themselves, but must cover their own user-account storage deposit. -variables = { - 'tankId': "efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou", #Specify the Fuel Tank ID to dispatch with - 'ruleSetId': 0, #Specify the ruleset to dispatch with - 'call': "MULTI_TOKENS", #Specify the pallet to use for the transaction - 'query': "mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}", #Insert the mutation to dispatch. - 'variables': None #If the mutation has variables, insert them here as an object. -} +### Coverage policy -response = requests.post('https://platform.beta.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - +The coverage policy controls what the tank pays for: -Once the mutation is sent, signed and broadcasted, If the transaction is eligible, the fuel tank will subsidize the transaction fees; otherwise, the transaction will fail. +- **Fees** — transaction fees only. +- **Fees and Deposit** — transaction fees plus any storage deposit a call requires (for example, listing a token locks a deposit until the listing is filled or cancelled). -:::info Need to broadcast from Managed Wallet? -To broadcast the transaction from a managed wallet account, add the `signingAccount` argument as instructed in the [Using Managed Wallets](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md#transferring-tokens-from-managed-wallets) page. -It's important to note that the `signingAccount` argument should be added on the `Dispatch` mutation level, and **NOT** in the dispatch argument. -In the above example, to transfer the token from a managed wallet via a fuel tank: +## Other configurations {#other-configurations} -```graphql -mutation Dispatch { - Dispatch( - tankId: "efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou" #Specify the Fuel Tank ID to dispatch with - ruleSetId: 0 #Specify the ruleset to dispatch with - signingAccount: "Managed Wallet Address Here" - dispatch: { - call: MULTI_TOKENS #Specify the pallet to use for the transaction - query: "mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}" #Insert the mutation to dispatch. - variables: null #If the mutation has variables, insert them here. - } - ){ - id - method - state - } -} -``` -::: \ No newline at end of file +The recommended setup is one combination of these rules, but they can be mixed for more targeted tanks — for example, a tank that only pays for batch transfers within a single collection, or one that caps each player at a fixed budget per month. Configure any of these on the create-tank form in the Console. For the complete rule reference, see the [Fuel Tank pallet](/04-enjin-blockchain/03-enjin-matrixchain/02-fuel-tank-pallet.md) documentation. diff --git a/static/img/guides/managing-users/v3-console-create-fuel-tank-button.png b/static/img/guides/managing-users/v3-console-create-fuel-tank-button.png new file mode 100644 index 0000000000000000000000000000000000000000..fec4d7ef7a8976e7e30fb920d5e942d29aed0539 GIT binary patch literal 99860 zcmeEuWmp^A_cc{oC{UxtDJ@c zkRri(hkI}D?|%M&e&>03!XzW-oY`lez1LcMg49&xNrTS)?VuQwor|#eYDwKg+~mGpWi&vzxssY8Ie_3%otVG02^uo z#D%O_)S1XMc&%4A0ztWz6xsX5O+iDn?ON=IA6F6$5xtKL(GO)zBDnb(JD>LISJ)K2 zOM6%QE9RRwZ+l44M|eJ++7z+u=}M`YukT$trp14&B2B*oH13}*{c=UDI4?aoIpMFz>wWKomz~D0wv!Q^uS2Y zK*U(+1OfMB70jlZ`2-d!WnjMk;~Jwlzk2(2CqIvUWC`&<*OHsa_lL%J2WzNp3Db^Y zZP%&d);;@IH$<}{$SNu72;PibT%$ecVQ6>tTXU7#)HnYO$2XCO9(yO7C%;BK1fShx z=t!b2_~4xcc3CYi@g9Myq-q<0@)j48(4$Urm2SshBcJpZ#@aBixCs4(_7A+~NFvXs zt;2>9r%D~NLVUdoqr{S2tAF&xCujd~(Bp5>o4UPlKO2gk{cJ40f0ewj%<2cm!q;S- z>baqDtz`TJa^hHSBjM8Ztm;`2*j_nRveTt97zP@(KxNAJPtQUT1g6cSzf$h7y6Au2 z->~I%3rlDaOEwE9)>sTxFdZK41Zm8OhCB}G++BvWp&l!+yMz&^8Rgk&n8b~a6xOHQ zZnv=A<1FSjxOUizk(8wlU+LegXkAn06(Rle4 zGugn7@LLt7*2^8?O!;pN5>yIN zeGg3I=P4cxWNJmvrB`qiDHYZKa0MZ(A{S-bO_n=Z70^-T&)bv?M5o8$_EALf3Sbm- z4)ZRe^TC(J*~aHwd5^%#kFm!(Aq#m{Fr`4!r%KUm;_g=P9J{u4={N9T?OtNH z!jUY~WE{@Zeslgpwt`+&VXIp_bHmA3 zm)8o&aM=LwTg29h4|DI?0LC!(K=z^<4R3Iy6^s zotdH&vw=kpi&9Ȋ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 O3qe{?UGC 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 hW=r~(vcM-^zS&2$CL}M`N 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 T!bXfm-dpaH(qEYujPLy)N~(LV literal 0 HcmV?d00001 diff --git a/static/img/guides/managing-users/v3-console-create-fuel-tank-form.png b/static/img/guides/managing-users/v3-console-create-fuel-tank-form.png new file mode 100644 index 0000000000000000000000000000000000000000..e9eca19668e75186028f9410b07180332cb68fa4 GIT binary patch literal 96626 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 Sv+3L0N67@@UM)TJ)vlOJyR94-o5PsE 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%0DI_vskG7adw*`2H9};A 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Ž?}HFe=tLkHPpYy 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

D`YTx0MD8x+^OM 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 literal 0 HcmV?d00001 diff --git a/static/img/guides/managing-users/v3-console-network-fuel-tanks-menu.png b/static/img/guides/managing-users/v3-console-network-fuel-tanks-menu.png new file mode 100644 index 0000000000000000000000000000000000000000..0155f153c34367e529013144ccfccc326250e84a GIT binary patch literal 29456 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 literal 0 HcmV?d00001 From 810a94ebda887e12e34eaf8d9995453b9762357d Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Thu, 28 May 2026 18:59:31 +0300 Subject: [PATCH 62/67] Clarify fuel tank rules intro and daemon address lookup Lead the recommended-setup section with what tank rules can do in general before introducing Require Signature by its end goal, and point readers at the Platform settings page for their daemon wallet address. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../01-platform/02-managing-users/04-using-fuel-tanks.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md b/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md index d90f379..b1fb0e6 100644 --- a/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md +++ b/docs/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md @@ -31,11 +31,11 @@ Fuel tanks are created and configured in the [Enjin Blockchain Console](https:// The most common use of a fuel tank is paying fees for players who never touch ENJ themselves. -Most games onboard players with [managed wallets](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md): the game creates and operates a wallet for each player, and the player can move everything to a self-custodial wallet later. Those players don't hold ENJ, so the game covers their fees — through a fuel tank that only your application can use. +A tank decides which transactions it pays for through a set of **rules**, which you can tailor to many different use cases — for example, a tank that only subsidizes transactions involving tokens from a specific collection, or one that covers fees only for a fixed list of whitelisted accounts. ([How fuel tanks work](#how-fuel-tanks-work) covers the full list.) -The key is the **Require Signature** rule. It tells the tank to accept a dispatch only if the call carries a valid signature from one specific account. Point that account at your Platform's **Wallet Daemon** address, and only your Platform account can dispatch through the tank — the platform attaches the daemon signature automatically on every transaction you send. No one else can dispatch through your tank, so it can't be drained or used to subsidize unrelated apps. +The setup below uses the most common rule, **Require Signature**, to build a tank that only your own application can dispatch through. That keeps the tank from being drained or used to subsidize unrelated apps, while your players transact without ever holding ENJ. -In short, the tank is configured with: +Most games onboard players with [managed wallets](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md): the game creates and operates a wallet for each player. To cover those players' fees, you configure the tank with: - **User account management** enabled, with the tank reserving the account-creation deposit, so players are added to the tank automatically and never pre-fund anything. - **Coverage policy** set to **Fees and Deposit**, so the tank pays both transaction fees and any storage deposit a call needs (for example, listing a token). @@ -73,7 +73,7 @@ Fill out the form: Click **Create fuel tank** and sign the transaction. Once it is on-chain, the new tank appears in the Fuel tanks list with its own address. :::tip Finding your daemon wallet address -The Require Signature rule must point at the address your Platform [Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) signs with — that's what lets the platform authorize dispatches through this tank automatically. The daemon prints its address on startup and it is the address that signs your transactions by default. +The Require Signature rule must point at the address your Platform [Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) signs with — that's what lets the platform authorize dispatches through this tank automatically. The quickest way to find it is the **Daemon address** field on your [Platform settings page](https://platform.beta.enjin.io/settings), where you can copy it directly. ::: ### 3. Fund the tank {#fund-tank} From 7d84a48a30bb4109fe83244f78db082691a7b541 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Thu, 28 May 2026 20:27:32 +0300 Subject: [PATCH 63/67] Add Using Proxies guide for delegated dispatching via proxyAddress New v3 page (no v2 equivalent). Leads with the cold-owner/hot-operator pattern using a Tokens proxy dispatched through proxyAddress, with a secondary player-delegation case. Adds a proxy glossary term. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../02-managing-users/05-using-proxies.md | 165 ++++++++++++++++++ src/glossary.js | 1 + .../v3-console-add-proxy-form.png | Bin 0 -> 95244 bytes .../v3-console-developer-extrinsics-menu.png | Bin 0 -> 51477 bytes 4 files changed, 166 insertions(+) create mode 100644 docs/02-guides/01-platform/02-managing-users/05-using-proxies.md create mode 100644 static/img/guides/managing-users/v3-console-add-proxy-form.png create mode 100644 static/img/guides/managing-users/v3-console-developer-extrinsics-menu.png diff --git a/docs/02-guides/01-platform/02-managing-users/05-using-proxies.md b/docs/02-guides/01-platform/02-managing-users/05-using-proxies.md new file mode 100644 index 0000000..a1b209c --- /dev/null +++ b/docs/02-guides/01-platform/02-managing-users/05-using-proxies.md @@ -0,0 +1,165 @@ +--- +title: "Using Proxies" +slug: "using-proxies" +description: "Delegate on-chain actions to another account with a proxy, so a hot operator wallet can act for a cold owner account without ever holding its keys." +--- + +import GlossaryTerm from '@site/src/components/GlossaryTerm'; + +A lets one account (the *delegate*) perform a limited set of actions on behalf of another account (the *owner*) without ever holding the owner's keys. The owner stays in full control: it decides which delegate to authorize, and a *proxy type* limits exactly which calls that delegate may make. + +Dispatching a transaction through a proxy takes a single optional argument: add `proxyAddress` to any `CreateTransaction` (or `CreateBatchTransaction`) and the platform runs that action on behalf of the given account. The signing account — your by default — must be a registered proxy of that account. + +This is different from a [managed wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md), where you hold the keys outright. With a proxy, the owner keeps its own keys and delegates only the powers you need. + +:::info What you'll need +- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). +- Two accounts: an **owner** account (the one whose actions you want to perform) and a **delegate** account that acts on its behalf (your [Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md)). +- Some ENJ on the owner account to cover the proxy deposit. You can get cENJ (Canary ENJ) for testing from the [built-in Canary faucet](/01-getting-started/04-using-the-enjin-platform.md#canary-faucet) in the Platform UI. +- A way to submit s on Enjin Matrixchain — the [Enjin Blockchain Console](https://console.enjin.io/) is used below — to register the proxy. +::: + +:::info Where proxies are created +A proxy is registered on-chain; creating one isn't part of the Enjin Platform yet, so this page uses the [Enjin Blockchain Console](https://console.enjin.io/). **Dispatching** through a proxy already works from the Platform API today (see [Dispatching through a proxy](#dispatching)). Platform support for managing proxies may be added later. +::: + +## Keep your owner account cold, operate with a hot wallet {#recommended-setup} + +The most common use of a proxy is operational security for your own project: keep the high-value account that owns your s offline, and let a separate hot wallet handle day-to-day work on its behalf. + +Because you're delegating from one of your own accounts to another, there's no third party to trust — you're effectively giving yourself a safer way to operate. If the hot wallet is ever compromised, you remove its proxy and your owner account and its assets are untouched. + +The setup is two steps: register your Wallet Daemon as a `Tokens` proxy of your owner account, then dispatch actions through it. + +### 1. Register the proxy {#register-proxy} + +From the **owner** account, authorize your Wallet Daemon to act on its behalf. + +Open the [Enjin Blockchain Console](https://console.enjin.io/) and make sure the network/chain selector in the top-left corner is set to the chain you want (for testing, use Canary Matrixchain). + +![The network and chain selector in the top-left of the Enjin Blockchain Console](/img/guides/managing-users/v3-console-network-chain-selector.png) + +In the top navbar, open **Developer → Extrinsics**. + +![The Developer menu open, with Extrinsics highlighted](/img/guides/managing-users/v3-console-developer-extrinsics-menu.png) + +Fill out the form: + +- **using the selected account** — choose your **owner** account (the account that owns, or will own, your assets and is granting the proxy). +- **submit the following extrinsic** — select **proxy** → **`addProxy(delegate, proxyType, delay)`**. +- **delegate** — your **Wallet Daemon** address. +- **proxyType** — **`Tokens`**, which lets the daemon perform any token operation (create, mint, transfer, burn, attributes, groups) on the owner's behalf. +- **delay** — **`0`**. + +![The Extrinsics submission form with proxy.addProxy selected, the delegate set to the Wallet Daemon, proxyType set to Tokens, and delay 0](/img/guides/managing-users/v3-console-add-proxy-form.png) + +Click **Submit Transaction** and sign with the owner account. This reserves a refundable of about 0.5 ENJ. Once it's on-chain, your daemon can act for the owner account. + +:::tip Finding your daemon wallet address +The **delegate** must be the address your Platform [Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) signs with. The quickest way to find it is the **Daemon address** field on your [Platform settings page](https://platform.beta.enjin.io/settings), where you can copy it directly. +::: + +### 2. Dispatch actions through the proxy {#dispatch-as-owner} + +Now any action you perform with `proxyAddress` set to the owner's address runs *as that owner*, signed by your daemon. For example, create a collection that belongs to your cold owner account: + +```graphql +mutation DispatchAsOwner { + CreateTransaction( + network: CANARY + chain: MATRIX + proxyAddress: "INSERT_OWNER_ADDRESS" #The account you keep cold and act on behalf of + transaction: { + createCollection: { forceCollapsingSupply: false } + } + ) { + uuid + action + state + } +} +``` + +Your daemon signs and submits the call as a proxy of the owner, so the new collection is owned by the **owner** account — not the daemon. The same pattern covers any action a `Tokens` proxy permits: swap `createCollection` for `mintToken`, `transferToken`, `batchSetTokenAttribute`, and so on. + +The daemon pays the for the call, while any the action reserves (for example, the 6.25 ENJ collection deposit) comes from the owner account, since the action runs as the owner. + +Once the transaction reaches `FINALIZED`, the chain emits the usual events for the action (here, the new `collection_id`). See [Working with Events](/05-enjin-platform/03-working-with-events.md). + +## Let players delegate to your app {#player-delegation} + +A proxy can also point the other way: a player who keeps their own self-custody wallet can grant *your* application a proxy, so you can manage their in-game items without holding their keys or prompting them to sign every action. The player registers your daemon as their delegate, exactly as in [step 1](#register-proxy) but signed from their own wallet, and you dispatch with `proxyAddress` set to the player's address. + +:::warning Use the least-privilege proxy type +Players are right to be cautious about authorizing anyone to move assets out of their wallet, so grant only what you need: + +- **`MultiTokensTransfer`** — the delegate can **only transfer** the player's tokens. Best for moving items (settling trades, distributing rewards). +- **`Tokens`** — also lets the delegate **burn** the player's tokens, which you need for crafting, consuming, or upgrade-by-burn mechanics. + +Avoid `Any`, which would also let the delegate move the player's ENJ. +::: + +```graphql +mutation MovePlayerItem { + CreateTransaction( + network: CANARY + chain: MATRIX + proxyAddress: "INSERT_PLAYER_ADDRESS" #The player who delegated a proxy to your daemon + transaction: { + transferToken: { + recipient: "INSERT_RECIPIENT_ADDRESS" + collectionId: 36105 + tokenId: 1 + amount: 1 + } + } + ) { + uuid + action + state + } +} +``` + +## Dispatching through a proxy {#dispatching} + +`proxyAddress` is available on both `CreateTransaction` and `CreateBatchTransaction`. Set it to an account's address and the platform runs your action on behalf of that account. The action executes exactly as if the owner had signed it directly — the only difference is who actually signs. + +A dispatch succeeds only if the signing account (your Wallet Daemon by default, or a [managed wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) selected with `signerExternalId`) is a registered proxy of the `proxyAddress` account, with a proxy type that permits the action. Otherwise the transaction fails. + +## How proxies work {#how-proxies-work} + +A proxy relationship has three parts: the **owner** (who grants it), the **delegate** (who acts), and a **proxy type** that limits which calls the delegate may make. + +### Proxy types + +The proxy type is the security boundary — always pick the narrowest one that covers the actions you dispatch. + +| Proxy type | What the delegate can do | +|---|---| +| `Any` | Every call, **including moving the owner's ENJ**. Most powerful and least safe — avoid unless you truly need it. | +| `Tokens` | All operations: create, mint, transfer, burn, attributes, and token groups. | +| `MultiTokensTransfer` | Only token transfers. The least-privilege option for delegates that just need to move tokens. | +| `Governance` | calls only. | +| `Staking` | Staking calls only (for example, bonding into nomination pools on the Relaychain). | + +### Deposits + +Registering a proxy reserves a refundable on the owner account — about 0.5 ENJ for the first proxy, plus a small amount per additional proxy. The deposit is returned when the proxy is removed. An account can hold up to 32 proxies. + +### Who pays + +The **delegate** (the account that signs — your Wallet Daemon, by default) pays the transaction fee for a proxied call, so it needs a little ENJ for fees. The owner pays nothing for the call itself, but still provides any storage deposit the action reserves, because the action runs as the owner. + +### Time-delayed proxies + +You can register a proxy with a `delay` (a number of blocks). The delegate must first announce a call with `proxy.announce`, and the owner has a window to cancel it with `proxy.rejectAnnouncement` before it can be executed. This adds a time-lock to high-value delegated actions. Most integrations use `delay: 0` (no delay). + +### Removing a proxy + +Remove a single proxy with `proxy.removeProxy(delegate, proxyType, delay)` — using the same arguments you registered it with — or clear them all with `proxy.removeProxies`. Removing a proxy returns its reserved deposit. + +## Other configurations {#other-configurations} + +- **Pure proxies.** `proxy.createPure` creates a brand-new keyless account that can only be controlled through its proxies — useful for team- or organization-owned collections where no single person should hold the key. This is advanced; only set it up if you understand how to recover access. +- **Actions in other pallets.** Some actions live outside s — for example, marketplace listings are part of the Marketplace . A `Tokens` or `MultiTokensTransfer` proxy won't cover those, so dispatch them from an account that owns them directly, or use a broader proxy type. diff --git a/src/glossary.js b/src/glossary.js index 15a6798..fcb2fd8 100644 --- a/src/glossary.js +++ b/src/glossary.js @@ -33,6 +33,7 @@ const glossary = { coin: { name: "Coin", definition: "Coins, like Enjin Coin, are integral to their own blockchain, primarily facilitating transactions and network security. While they are central currencies, blockchains like Enjin's also host tokens, representing varied assets or utilities, thereby enriching the ecosystem beyond just the native currency, ensuring a multifunctional platform." }, pallet: { name: "Pallet", definition: "A pallet is a module or plugin in the Enjin Blockchain that contains a specific set of features and functionalities. Pallets allow developers to add new features and capabilities to the blockchain, facilitating customization and the development of various decentralized applications and services. Each pallet contains the code and logic required to execute a specific function within the Enjin Ecosystem." }, fuel_tank: { name: "Fuel Tank", definition: "A fuel tank is a shared fund used by multiple accounts for transactions. It's created and adjusted with specific commands and has rules determining how it pays transaction fees and provides storage deposits." }, + proxy: { name: "Proxy", definition: "A proxy is an account authorized to perform a limited set of actions on behalf of another account, without ever holding that account's private keys. The account granting the authority (the owner) registers a delegate account together with a proxy type that defines which calls the delegate may make. Proxies let a high-value account stay offline while a separate, lower-risk account handles day-to-day operations. The reserved deposit is refunded when the proxy is removed." }, pool: { name: "Pool", definition: "A staking pool is a group of holders who combine their resources to participate in a network's proof-of-stake (PoS) consensus mechanism. By joining a staking pool, individual stakeholders have a higher chance of being chosen to validate transactions and earn rewards, compared to staking alone. The rewards are then distributed among the pool participants based on their contributions." }, graphql: { name: "GraphQL", definition: "GraphQL is a query language for APIs, enabling clear definitions of the structure of API requests. It allows clients to request exactly the data they need, nothing more or less, making it flexible and efficient. Developed by Facebook, GraphQL provides a more powerful and flexible way to interact with databases through APIs compared to REST." }, sdk: { name: "SDK", definition: "An SDK, or Software Development Kit, is a set of tools, libraries, code samples, processes, and guides that developers use to create applications for specific programming environments, operating systems, or platforms. It simplifies the development process, allowing developers to focus more on application design and logic rather than mundane, repetitive tasks." }, diff --git a/static/img/guides/managing-users/v3-console-add-proxy-form.png b/static/img/guides/managing-users/v3-console-add-proxy-form.png new file mode 100644 index 0000000000000000000000000000000000000000..bed54cae812d71c729d385f7efbb73a6519c63f5 GIT binary patch literal 95244 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 z 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
!Kr*=W6;6dlJ)E~zd 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 literal 0 HcmV?d00001 diff --git a/static/img/guides/managing-users/v3-console-developer-extrinsics-menu.png b/static/img/guides/managing-users/v3-console-developer-extrinsics-menu.png new file mode 100644 index 0000000000000000000000000000000000000000..81d5cafe099ac13eeee230590212a02a25a58a74 GIT binary patch literal 51477 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 Date: Thu, 28 May 2026 20:45:36 +0300 Subject: [PATCH 64/67] Fix broken WalletConnect link in transferring-tokens and freezing-thawing --- .../01-platform/01-managing-tokens/05-transferring-tokens.md | 2 +- .../01-platform/01-managing-tokens/07-freezing-thawing.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md index 2330781..cc29e59 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md +++ b/docs/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md @@ -691,7 +691,7 @@ To transfer multiple tokens **from a single collection** to multiple recipients :::tip Need to send a transaction request to user's wallet? This can be done using Enjin Platform API & WalletConnect! -To learn more, check out the [Using WalletConnect page](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md). +To learn more, check out the [Using WalletConnect page](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-using-wallet-connect.md). ::: Once a transfer transaction reaches `FINALIZED`, a `MultiTokens.Transferred` event (for token transfers) or `Balances.Transfer` event (for ENJ transfers) is emitted with the sender, recipient, and amount — useful for reacting to inbound transfers in real time (e.g. unlocking an in-game item the moment a player receives the corresponding NFT). See [Working with Events](/05-enjin-platform/03-working-with-events.md) for how to read it. diff --git a/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md b/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md index 637e5aa..c402002 100644 --- a/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md +++ b/docs/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md @@ -948,5 +948,5 @@ To sign with a managed wallet instead of the Wallet Daemon, set `signerAccount` :::tip Need to send a transaction request to user's wallet? This can be done using Enjin Platform API & WalletConnect! -To learn more, check out the [Using WalletConnect page](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md). +To learn more, check out the [Using WalletConnect page](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-using-wallet-connect.md). ::: From 9d9153088ceb3835493832300b0feea6d863e2eb Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 1 Jun 2026 13:38:59 +0300 Subject: [PATCH 65/67] @ Remove v2 versioning and beta banner; make v3 the only docs version Treat v3 as the main, only-released docs now that v2 registration is closed and v2 is being retired: - Strip the versions config (lastVersion, includeCurrentVersion, the v3 (Beta)/v2 version map and unreleased banner) so the current docs serve at the site root with no version label and no beta banner. - Remove the version dropdown from the navbar. - Delete the frozen v2 content: versions.json, versioned_docs/, and versioned_sidebars/. Also hide the in-progress Fetching Token Holders guide by prefixing its filename with "_" (no backlinks to it; sidebar is autogenerated). Co-Authored-By: Claude Opus 4.8 (1M context) @ --- .gitignore | 1 + ...lders.md => _09-fetching-token-holders.md} | 0 docusaurus.config.js | 17 - .../01-getting-started/01-introduction.md | 69 - .../02-quick-start-guide.md | 289 ----- .../04-using-the-enjin-platform.md | 589 --------- .../01-how-to-use-graphql.md | 792 ------------ .../05-using-enjin-api/05-using-enjin-api.md | 98 -- .../05-using-enjin-api/_category_.json | 3 - .../06-using-wallet-daemon.md | 172 --- .../01-getting-started/_category_.json | 8 - .../01-creating-collections.md | 271 ---- .../010-transfer-accept-collection.md | 343 ----- .../02-creating-a-currency-token.md | 286 ----- .../02-creating-tokens/02-creating-tokens.md | 364 ------ .../02-creating-tokens/_category_.json | 4 - .../01-managing-tokens/03-adding-metadata.md | 456 ------- .../01-managing-tokens/04-minting-a-token.md | 339 ----- .../05-transferring-tokens.md | 724 ----------- .../01-managing-tokens/06-create-qr-drops.md | 995 --------------- .../01-managing-tokens/07-freezing-thawing.md | 1042 --------------- .../08-burning-destroying-tokens.md | 640 ---------- .../09-fetching-token-holders.md | 670 ---------- .../01-managing-tokens/_category_.json | 4 - .../01-using-wallet-connect.md | 237 ---- .../02-verifying-wallets.md | 717 ----------- .../_category_.json | 3 - .../02-reading-user-wallets.md | 915 -------------- .../03-using-managed-wallets.md | 975 -------------- .../02-managing-users/04-using-fuel-tanks.md | 1057 ---------------- .../02-managing-users/_category_.json | 3 - .../01-tokenid-structure.md | 188 --- .../02-metadata-example.md | 104 -- .../02-metadata-standard.md | 79 -- .../03-dynamic-metadata.md | 188 --- .../02-metadata-standard/_category_.json | 3 - .../03-enforced-rarity.md | 31 - .../04-brand-collab-nfts.md | 218 ---- .../03-advanced-mechanics/05-enj-infusion.md | 224 ---- .../06-the-multiverse.md | 169 --- .../03-advanced-mechanics/_category_.json | 3 - ..._dispatch-transactions-using-fuel-tanks.md | 5 - .../01-getting-started.md | 208 --- .../02-graphql-requests.md | 467 ------- .../03-platform-events.md | 392 ------ .../04-software-development-kit.md | 15 - .../_category_.json | 3 - .../04-software-development-kit/_logger.md | 170 --- .../01-setup-guide.md | 179 --- .../03-implementation-breakdown.md | 185 --- .../_category_.json | 3 - .../01-platform/06-enjin-discord-bot.md | 62 - .../02-guides/01-platform/_category_.json | 8 - .../02-staking-integrtion-guide.md | 669 ---------- .../03-creating-nomination-pools.md | 91 -- .../04-joining-nomination-pools.md | 85 -- .../01-relaychain/_category_.json | 7 - .../01-enjin-blockchain-nodes.md | 54 - .../01-running-a-node.md | 67 - .../02-run-relaychain-node.md | 128 -- .../03-run-matrixchain-node.md | 128 -- .../01-enjin-blockchain-nodes/_category_.json | 4 - .../01-running-a-validator.md | 210 ---- ...-security-best-practices-for-validators.md | 48 - .../03-how-to-stop-validating.md | 66 - .../_category_.json | 7 - .../02-running-nodes/_category_.json | 7 - .../02-blockchain/03-running-enjin-indexer.md | 220 ---- .../02-guides/02-blockchain/_category_.json | 8 - .../version-2.x/02-guides/_category_.json | 7 - .../01-queries/01-transactions-queries.md | 1116 ----------------- .../01-queries/02-collections-queries.md | 405 ------ .../01-queries/03-tokens-queries.md | 317 ----- .../01-queries/04-wallets-queries.md | 990 --------------- .../01-queries/05-fuel-tank-queries.md | 283 ----- .../01-queries/06-marketplace-queries.md | 593 --------- .../01-queries/07-beam-queries.md | 613 --------- .../01-queries/_category_.json | 4 - .../02-mutations/01-transaction-mutations.md | 94 -- .../02-mutations/02-collections-mutations.md | 495 -------- .../02-mutations/03-tokens-mutations.md | 1033 --------------- .../02-mutations/04-wallets-mutations.md | 125 -- .../02-mutations/05-fuel-tanks-mutations.md | 827 ------------ .../02-mutations/06-marketplace-mutations.md | 349 ------ .../02-mutations/07-beam-mutations.md | 130 -- .../02-mutations/_category_.json | 4 - .../03-api-reference/03-api-reference.md | 93 -- .../03-api-reference/03-websocket-events.md | 41 - .../04-important-arguments.md | 434 ------- .../03-api-reference/_category_.json | 3 - .../01-validator-staking.md | 125 -- .../02-enjin-relaychain.md | 35 - .../02-stake-exchange-pallet.md | 175 --- .../02-enjin-relaychain/03-slashing.md | 49 - .../02-enjin-relaychain/_category_.json | 4 - .../01-multitoken-pallet.md | 328 ----- .../02-fuel-tank-pallet.md | 370 ------ .../03-enjin-matrixchain.md | 22 - .../03-marketplace-pallet.md | 151 --- .../03-enjin-matrixchain/_category_.json | 4 - .../04-enjin-blockchain.md | 26 - .../04-enjin-blockchain/_category_.json | 4 - .../05-enjin-platform/02-chain-state.md | 99 -- .../03-working-with-events.md | 177 --- .../02-local-installation.md | 187 --- .../04-enterprise-on-prem/03-faq.md | 33 - .../04-enterprise-on-prem.md | 87 -- .../04-laravel-horizon.md | 18 - .../05-extending-the-platform.md | 88 -- .../_Telemetry_02-local-installation.md | 197 --- .../04-enterprise-on-prem/_category_.json | 3 - .../05-enjin-platform/05-enjin-platform.md | 107 -- .../05-enjin-platform/_category_.json | 4 - .../06-enjin-products/01-enjin-tech-stack.md | 31 - .../06-enjin-products/02-enjin-coin.md | 45 - .../06-enjin-products/03-enjin-wallet.md | 24 - .../06-enjin-products/04-nftio-marketplace.md | 26 - .../version-2.x/06-enjin-products/05-beam.md | 68 - .../06-enjin-products/_category_.json | 8 - versioned_sidebars/version-2.x-sidebars.json | 8 - versions.json | 3 - 121 files changed, 1 insertion(+), 27247 deletions(-) rename docs/02-guides/01-platform/01-managing-tokens/{09-fetching-token-holders.md => _09-fetching-token-holders.md} (100%) delete mode 100644 versioned_docs/version-2.x/01-getting-started/01-introduction.md delete mode 100644 versioned_docs/version-2.x/01-getting-started/02-quick-start-guide.md delete mode 100644 versioned_docs/version-2.x/01-getting-started/04-using-the-enjin-platform.md delete mode 100644 versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md delete mode 100644 versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/05-using-enjin-api.md delete mode 100644 versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/_category_.json delete mode 100644 versioned_docs/version-2.x/01-getting-started/06-using-wallet-daemon.md delete mode 100644 versioned_docs/version-2.x/01-getting-started/_category_.json delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/01-creating-collections.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/_category_.json delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/_category_.json delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/_category_.json delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/_category_.json delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-example.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/_category_.json delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/03-enforced-rarity.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/04-brand-collab-nfts.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/05-enj-infusion.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/06-the-multiverse.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/_category_.json delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/_dispatch-transactions-using-fuel-tanks.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/01-getting-started.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/02-graphql-requests.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/03-platform-events.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/_category_.json delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/_logger.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/03-implementation-breakdown.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/_category_.json delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/06-enjin-discord-bot.md delete mode 100644 versioned_docs/version-2.x/02-guides/01-platform/_category_.json delete mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/02-staking-integrtion-guide.md delete mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/03-creating-nomination-pools.md delete mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/04-joining-nomination-pools.md delete mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/_category_.json delete mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-enjin-blockchain-nodes.md delete mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-running-a-node.md delete mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/02-run-relaychain-node.md delete mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/03-run-matrixchain-node.md delete mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/_category_.json delete mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md delete mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/02-security-best-practices-for-validators.md delete mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/03-how-to-stop-validating.md delete mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/_category_.json delete mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/_category_.json delete mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/03-running-enjin-indexer.md delete mode 100644 versioned_docs/version-2.x/02-guides/02-blockchain/_category_.json delete mode 100644 versioned_docs/version-2.x/02-guides/_category_.json delete mode 100644 versioned_docs/version-2.x/03-api-reference/01-queries/01-transactions-queries.md delete mode 100644 versioned_docs/version-2.x/03-api-reference/01-queries/02-collections-queries.md delete mode 100644 versioned_docs/version-2.x/03-api-reference/01-queries/03-tokens-queries.md delete mode 100644 versioned_docs/version-2.x/03-api-reference/01-queries/04-wallets-queries.md delete mode 100644 versioned_docs/version-2.x/03-api-reference/01-queries/05-fuel-tank-queries.md delete mode 100644 versioned_docs/version-2.x/03-api-reference/01-queries/06-marketplace-queries.md delete mode 100644 versioned_docs/version-2.x/03-api-reference/01-queries/07-beam-queries.md delete mode 100644 versioned_docs/version-2.x/03-api-reference/01-queries/_category_.json delete mode 100644 versioned_docs/version-2.x/03-api-reference/02-mutations/01-transaction-mutations.md delete mode 100644 versioned_docs/version-2.x/03-api-reference/02-mutations/02-collections-mutations.md delete mode 100644 versioned_docs/version-2.x/03-api-reference/02-mutations/03-tokens-mutations.md delete mode 100644 versioned_docs/version-2.x/03-api-reference/02-mutations/04-wallets-mutations.md delete mode 100644 versioned_docs/version-2.x/03-api-reference/02-mutations/05-fuel-tanks-mutations.md delete mode 100644 versioned_docs/version-2.x/03-api-reference/02-mutations/06-marketplace-mutations.md delete mode 100644 versioned_docs/version-2.x/03-api-reference/02-mutations/07-beam-mutations.md delete mode 100644 versioned_docs/version-2.x/03-api-reference/02-mutations/_category_.json delete mode 100644 versioned_docs/version-2.x/03-api-reference/03-api-reference.md delete mode 100644 versioned_docs/version-2.x/03-api-reference/03-websocket-events.md delete mode 100644 versioned_docs/version-2.x/03-api-reference/04-important-arguments.md delete mode 100644 versioned_docs/version-2.x/03-api-reference/_category_.json delete mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/01-validator-staking.md delete mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/02-enjin-relaychain.md delete mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/02-stake-exchange-pallet.md delete mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/03-slashing.md delete mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/_category_.json delete mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md delete mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/02-fuel-tank-pallet.md delete mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/03-enjin-matrixchain.md delete mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/03-marketplace-pallet.md delete mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/_category_.json delete mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/04-enjin-blockchain.md delete mode 100644 versioned_docs/version-2.x/04-enjin-blockchain/_category_.json delete mode 100644 versioned_docs/version-2.x/05-enjin-platform/02-chain-state.md delete mode 100644 versioned_docs/version-2.x/05-enjin-platform/03-working-with-events.md delete mode 100644 versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/02-local-installation.md delete mode 100644 versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/03-faq.md delete mode 100644 versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/04-enterprise-on-prem.md delete mode 100644 versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/04-laravel-horizon.md delete mode 100644 versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/05-extending-the-platform.md delete mode 100644 versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/_Telemetry_02-local-installation.md delete mode 100644 versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/_category_.json delete mode 100644 versioned_docs/version-2.x/05-enjin-platform/05-enjin-platform.md delete mode 100644 versioned_docs/version-2.x/05-enjin-platform/_category_.json delete mode 100644 versioned_docs/version-2.x/06-enjin-products/01-enjin-tech-stack.md delete mode 100644 versioned_docs/version-2.x/06-enjin-products/02-enjin-coin.md delete mode 100644 versioned_docs/version-2.x/06-enjin-products/03-enjin-wallet.md delete mode 100644 versioned_docs/version-2.x/06-enjin-products/04-nftio-marketplace.md delete mode 100644 versioned_docs/version-2.x/06-enjin-products/05-beam.md delete mode 100644 versioned_docs/version-2.x/06-enjin-products/_category_.json delete mode 100644 versioned_sidebars/version-2.x-sidebars.json delete mode 100644 versions.json diff --git a/.gitignore b/.gitignore index 4121f1f..cc67034 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ yarn-error.log* # v3 documentation migration working files (not for upstream) v3-*.md v3-*.json +summary.md build-test/ diff --git a/docs/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md b/docs/02-guides/01-platform/01-managing-tokens/_09-fetching-token-holders.md similarity index 100% rename from docs/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md rename to docs/02-guides/01-platform/01-managing-tokens/_09-fetching-token-holders.md diff --git a/docusaurus.config.js b/docusaurus.config.js index c9999ec..278a9dd 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -46,19 +46,6 @@ const config = { routeBasePath: '/', // Serve the docs at the site's root sidebarPath: './sidebars.js', editUrl: 'https://github.com/enjin/docs/tree/master/', - lastVersion: '2.x', - includeCurrentVersion: true, - versions: { - current: { - label: 'v3 (Beta)', - path: 'v3', - banner: 'unreleased', - }, - '2.x': { - label: 'v2', - path: '', - }, - }, }, blog: false, theme: { @@ -104,10 +91,6 @@ const config = { {href: 'https://enjin.io/changelog', label: 'Changelog', position: 'left'}, {href: '/tools', label: 'Tools', position: 'left'}, - { - type: 'docsVersionDropdown', - position: 'right', - }, { href: 'https://console.enjin.io', label: 'Blockchain Console', diff --git a/versioned_docs/version-2.x/01-getting-started/01-introduction.md b/versioned_docs/version-2.x/01-getting-started/01-introduction.md deleted file mode 100644 index 0c21548..0000000 --- a/versioned_docs/version-2.x/01-getting-started/01-introduction.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "Introduction to the Enjin Ecosystem" -slug: "introduction" -description: "Explore Enjin: the NFT-focused blockchain ecosystem purpose-built for games & apps. Discover its Platform API, SDKs (C#, C++), Fuel Tanks, and Managed Wallets" ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; - -![Welcome to the Enjin Ecosystem](/img/getting-started/enjin-ecosystem.png) - -Welcome to Enjin! You're stepping into a comprehensive ecosystem designed for building the next generation of games and applications powered by (NFTs). This page provides an overview of what Enjin offers, its key components, and the powerful capabilities you can leverage. - -## What is Enjin? - -Enjin is a pioneering platform engineered to support the dynamic activity, numerous digital items, and high performance demanded by modern games and applications. Its most distinctive feature is the **native integration of s at the 's protocol level**. This foundational design significantly simplifies development, enhances security, and facilitates true digital ownership, making it easier than ever to create, manage, and integrate s. - -## Understanding the Enjin Ecosystem -Enjin is more than just a ; it's a suite of interconnected tools and services designed to work together seamlessly: - -

- -
- -- **Enjin Blockchain:** The core of the ecosystem, is a **, dual-layer chain** built for **scalability, security, and ease of use** in game and app development. As a purpose-built for these applications, its protocol-level functionalities optimize creation, management, and transactions, ensuring efficiency and reliability. -*[Learn more about Enjin Blockchain Architecture](/04-enjin-blockchain/04-enjin-blockchain.md)* -- **The :** This platform is your primary gateway to the . Its robust API allows you to manage digital assets, interact with users, and implement features without requiring deep expertise. -*[Explore the API Reference](/03-api-reference/03-api-reference.md)* -- **Software Development Kits (SDKs) & Code Snippets:** To accelerate your development, Enjin offers official **[s](/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md) (primarily in C# and C++)**. You'll also find **code snippets for common actions in , cURL, JavaScript, Node.js, and Python**. These resources include comprehensive documentation and examples to streamline integration. -*[Check out the SDKs page](/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md)* -- **Key Enjin Products:** - - **:** A secure, user-friendly mobile wallet for storing, managing, and trading s and other digital assets from the Enjin ecosystem and beyond. -*[Learn more about the Enjin Wallet](/06-enjin-products/03-enjin-wallet.md)* - - **:** The official marketplace for the Enjin ecosystem, where users can discover, buy, sell, and trade s, fostering a vibrant economy for your digital assets. - *[Visit NFT.io](https://nft.io)* - - **:** A powerful marketing tool for distributing s in bulk via QR codes, links, or other methods—ideal for airdrops, rewards, and promotional campaigns. - *[Discover Enjin Beam](/06-enjin-products/05-beam.md)* - -## What You Can Achieve with Enjin - -Enjin empowers you to build innovative experiences by leveraging its unique features: - -1. **Create and Manage Digital Assets Securely:** - Easily design, , and oversee your s and individual s. The 's native support for s ensures inherent security, efficiency, and standardized operations for all your digital assets. -2. **Seamlessly Integrate NFTs into Games and Apps:** - Use the API and s to effortlessly embed rich functionalities. tokens on-demand, program asset transfers, implement freezing mechanics, retrieve holder information, and execute a wide array of interactions with ease. -3. **Simplify User Onboarding with Managed Wallets:** - Lower the entry barrier for users new to cryptocurrency by managing their wallets via the . Users can enjoy seamless interactions and later choose to export their assets to their self-custody , significantly streamlining the Web2-to-Web3 transition. - *[Read about Managed Wallets](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md)* -
- -
-4. **Enhance User Experience with Fuel Tanks:** - Improve the experience for users, especially those new to Web3, by subsidizing using s. Create and fund on-chain pools with dynamic eligibility rules to abstract away fee complexities, making interactions smoother. - *[Read about Fuel Tanks](/04-enjin-blockchain/03-enjin-matrixchain/02-fuel-tank-pallet.md)* - -## Enjin Ecosystem Slack - -Join the official [Enjin Ecosystem Slack community]( -https://enj.in/ecosystem-slack) to connect with other developers and community members. It's a great place to: - -- Ask questions and get help -- Share your projects and ideas -- Interact directly with Atlas Development, a core contributor to Enjin - -## Ready to Start Building? - -This overview offers a glimpse into the Enjin ecosystem. To get hands-on and begin your development journey, our Quick Start Guide is the perfect next step. - -➡️ **[Proceed to the Quick Start Guide](/01-getting-started/02-quick-start-guide.md)** \ No newline at end of file diff --git a/versioned_docs/version-2.x/01-getting-started/02-quick-start-guide.md b/versioned_docs/version-2.x/01-getting-started/02-quick-start-guide.md deleted file mode 100644 index dca7742..0000000 --- a/versioned_docs/version-2.x/01-getting-started/02-quick-start-guide.md +++ /dev/null @@ -1,289 +0,0 @@ ---- -title: "Quick Start Guide" -slug: "quick-start-guide" -description: "Begin your journey with Enjin's blockchain ecosystem using our Quick Start Guide. Learn how to integrate blockchain items into your projects seamlessly" ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; - -# Enjin Quick Start Guide - -Welcome! This guide provides the essential steps to quickly integrate the and s into your game or application using the . We'll use the testnet for a safe and free development environment. - -:::info **By the end of this guide, you will have:** - -- Set up your developer account on the Canary . -- Connected your developer wallet to authorize actions. -- Created your first blockchain and . -- Learned how to link your users' s to your application. - - Sent a token to a linked user wallet. - - Read tokens from a linked user wallet -::: - -## 1. Create Your Enjin Canary Platform Account - -### What is the Enjin Platform? - -The Enjin Platform is your central hub for managing blockchain projects, collections, and digital items (NFTs and Fungible Tokens). We'll start by creating an account on the Canary Platform, which operates on Enjin's testnet blockchain. This allows you to experiment without using real funds or affecting the main network. - -### Steps to Create an Account: - -1. Navigate to the Canary Platform website: https://platform.canary.enjin.io/ -2. Click on the "Create an account" button. -3. Fill in your account credentials and click on the "Register" button. -4. Verify your email address by clicking the link sent to your inbox. -5. Log in to your newly created Canary Platform account. - -## 2. Connect Your Wallet Account - -### Why Connect a Wallet? - -To perform actions on the blockchain (like creating collections or minting tokens) through the Platform, you need to authorize these actions. Connecting your developer wallet (like the Enjin Wallet app) allows the Platform to request your signature for transactions you initiate. - -### Steps to Connect Your Wallet: - -To connect your wallet, click on the "Connect Wallet" button in the top right corner and follow the on-screen instructions. - -![Connecting a wallet app on the Enjin Platform](/img/getting-started/connect-wallet-app.gif) - -:::tip Automating Signatures with the -For applications requiring automated actions (like minting rewards in real-time or batch transferring items without manual confirmation each time), you can set up the **Enjin Wallet Daemon**. This secure service runs on your server and can automatically sign transaction requests sent to your Enjin Platform. - -**Learn More:** [Set up the Enjin Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) -::: - -## 3. Create Your First Collection and Token - -### What are Collections and Tokens? -Let's define the core building blocks for your digital items on the : - -- **Collection:** Think of a as a container or category for your digital items. It helps organize items related to your specific game, application, or a particular series. All items within a collection share certain base properties defined when you create the collection. -- **Token (`multiToken`):** A **Token** represents the actual digital item that users can own within a Collection. On the Enjin Blockchain, all tokens fundamentally exist as **s**. This is a core feature built directly into the blockchain protocol itself. - -:::tip Structuring Your Project -A common approach is to create: -- **One Collection** for your entire game or application. -- **Multiple Tokens** within that collection, representing the various items, characters, achievements, or currencies your users can own. -::: - -### Steps for creating a collection and token: - -:::info **Ways to Create Blockchain Requests:** -You can interact with the Enjin Platform and initiate blockchain requests in several ways: -- **Manually:** Using the user interface of the Enjin Platform Cloud. -- **Programmatically:** - - Sending **direct HTTP requests** to the [Enjin GraphQL API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md). - - Using the [**official Enjin SDKs**](/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md): - - [C# SDK](https://github.com/enjin/platform-csharp-sdk) - - [C++ SDK](https://github.com/enjin/platform-cpp-sdk) - - **Other Languages:** The [Guides section](/category/integration-guides) provides code snippets for GraphQL, cURL, JavaScript, Node.JS, and Python for most operations. - - **AI Assistant:** Utilize the [Enjin Platform AI assistant](https://chatgpt.com/g/g-678f70643d2c8191a78baff699e46e5f-enjin-platform-ai) to help construct API calls. -::: - -1. **Create Collection:** - - In the Platform menu, navigate to the **[Collections page](https://platform.canary.enjin.io/collections)**. Then, click on the "**[Create Collection](https://platform.canary.enjin.io/create/collection)**" button. - ![Creating a collection](/img/guides/managing-tokens/create-collection.gif) - - Customize your collection - you can specify the collection's name, description, media, attributes, Mint and Market policies, and more. - To learn more about collection configuration, please check the [Create Collections guide](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md). - - Submit the creation request by clicking on the **Create** button. This will create a signature request which you can view in the [Transactions page](https://platform.canary.enjin.io/transactions), and will require a transaction to be signed. - ![](/img/guides/managing-tokens/pending-create-collection-txn.png) - - To sign the Create Collection transaction request - Click on the **"Sign"** button and approve the signature request with your connected wallet. - - Once the transaction is confirmed on-chain, the new collection will appear in your [Collections page](https://platform.canary.enjin.io/collections), where you can also find its ID, keep note of it for the next step. -2. **Create Token:** - - In the Platform menu, navigate to the **[Tokens page](https://platform.canary.enjin.io/tokens)**. Then, click on the "**[Create Token](https://platform.canary.enjin.io/create/token)**" button. - - Customize your token - you can specify the token's name, description, media, attributes, and more. - To learn more about token configuration, please check the [Create Tokens guide](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md). - - In the **"Collection ID"** input field, insert the collection id of the collection you just created. You may also click the input field and select the id from the dropdown menu. - - In the **"Recipient"** input field, insert the of the wallet that you wish to receive the token's initial supply. You can find the address of your connected wallet in the [settings page](https://platform.canary.enjin.io/settings) by clicking on the Settings icon in the top right corner, followed by the "Settings" button. - - Submit the creation request by clicking on the **Create** button. This requires another transaction, which you can sign in the [Transactions page](https://platform.canary.enjin.io/transactions). - -:::tip Programmatic Creation -You can also create collections and tokens using the API or SDKs. - -➡️ **See Guides:** -- [Guide: Create Collections Programmatically](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md#option-b-using-the-enjin-api--sdks) -- [Guide: Create Tokens Programmatically](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md#option-b-using-the-enjin-api--sdks) -::: - -## 4. Link a User's Wallet to Your Application - -### Why Link User Wallets? - -To interact with a specific user's blockchain items (check their holdings, send them items), you need to know their public wallet address and have proof they control it. This process involves **verifying** the user's ownership of their wallet and then **associating** that verified wallet address with their account within your application's database. This association is what allows you to connect their in-app identity with their blockchain identity. - -The typical flow for this is: - -1. Your application requests a unique wallet verification QR code from the Enjin API. -2. Your application displays this QR code to the user. -3. The user scans the QR code with their Enjin Wallet app. -4. The wallet confirms ownership, securely providing your application with the user's public wallet address. -5. Your application then saves this verified wallet address and **associates** it with the user's internal ID in your database. - -Once this association (or "link") is established, you can: - - - Grant in-game perks based on NFT ownership. - - Airdrop or transfer items directly to the user's associated wallet. - - Display their relevant blockchain inventory within your app. - -### Implementation (GraphQL Example): - -To request a wallet verification code, we need to interact with the **** -The Enjin Platform API is a API. If you are unfamiliar with GraphQL, we recommend checking out the [How to Use GraphQL](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md) guide. - -:::info Quick Start: Sending API Requests with the -In this example, we will be using the to easily send an API request from within the browser. -In a production-ready integration, your app will typically send API calls using HTTP requests or one of the [official Enjin SDKs](/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md). You can learn more about this in the [Using the Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) page. -::: - -To interact with the Enjin Platform API, head over to the [Enjin Platform](https://platform.canary.enjin.io/), click on the settings icon in the top right corner, followed by **"platform-core"** in the available packages. This will navigate to the [Enjin Platform GraphiQL Playground](https://platform.canary.enjin.io/graphiql/). - -Once you are in the GraphiQL playground page, copy & paste the following query: - -```graphql -query RequestAccount{ - RequestAccount{ - qrCode #Returns a QR code for your user to scan - verificationId #An id to this account request call. Save this for next step - } -} -``` - -This will return a link to a QR Code for you to display to your user, and a unique verification id to track the request. -In your application, you'll display this QR code to your user and instruct them to scan it with their . - -To confirm that the user scanned the QR code and approved the request, copy & paste the following query (replace the `replace_verification_id_here` placeholder with the `verificationId` from the previous call's response): - -```graphql -query GetAccountVerified{ - GetAccountVerified(verificationId: "replace_verification_id_here"){ - verified - account{ - publicKey - address - } - } -} -``` - -If the user successfully scanned the QR code and approved the request, the response will show `"verified": true` and contain the user's wallet address: - -```json -{ - "data": { - "GetAccountVerified": { - "verified": true, - "account": { - "publicKey": "0x5a6aae294416f3e875d9a8975658905002cfd3e5e64105d76296c4b0adbfd77e", //The public key of the verified account - "address": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //The address of the verified account - } - } - } -} -``` - -At this point, your application should take the `address` value from the response and store it in your user database, associated with the specific user who scanned the QR code. This is the crucial step that "links" the user's in-app identity with their blockchain wallet address. - -:::tip Learn more about linking user wallets -Check the full in-depth guide: [Linking User Wallets](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md). -::: - -## 5. Send a Token to a User - -### Why Send Tokens? - -This is a core interaction – transferring an NFT from your project's wallet (or another source) to a user's linked wallet. This could be for: -- Rewarding achievements. -- In-app purchases. -- Airdrops or giveaways. - -### Implementation (GraphQL Example): - -Sending tokens involves creating a transfer request via the API. You'll need the Token ID, the recipient's linked wallet address, and the amount (for s). - -Copy & paste the following mutation into the GraphiQL playground: - -```graphql -mutation TransferToken{ - SimpleTransferToken( - collectionId: 36105 #Specify the collection ID here - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the recipent address here - params: { - tokenId: {integer: 0} #Specify the token ID - amount: 1 #Choose the transfer amount - } - ){ - id - method - state - } -} -``` - -:::note This request needs to be signed. -You can sign it in the [Transactions page](https://platform.canary.enjin.io/transactions), as we did in the previous steps. -::: - -:::tip Learn more about sending tokens -Check the full in-depth guide: [Sending Tokens](/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md). -::: - -## 6. Reading Linked User's Tokens - -### Why Read User Tokens? - -Once a user has linked their wallet (as shown in [Step 4](#4-link-a-users-wallet-to-your-application)), you'll often need to check which tokens they own within your collection(s). This allows you to: - -- Display their relevant inventory within your game or application interface (like in-game currency or an item). -- Verify ownership of specific NFTs to grant access, perks, or abilities. -- Trigger game logic based on the items they hold. - -### Implementation (GraphQL Example): - -To read the user's wallet, copy & paste the following query into the GraphiQL playground, using the user's linked wallet address: - -```graphql -query FetchingWalletTokens{ - GetWallet(account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"){ #Specify the account address - tokenAccounts{ - edges{ - node{ - balance - token{ - tokenId - collection{ - collectionId - } - attributes{ - key - value - } - } - } - } - } - } -} -``` - -:::tip Learn more about reading user wallet balances -Check the full in-depth guide: [Reading User Wallet Balances](/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md). -::: - -## 7. Next Steps and Advanced Features - -Congratulations! You've covered the basics of setting up your project on the Enjin Platform, creating items, and enabling user wallet interactions. - -:::warning **Ready for Production?** -Remember, this guide used the **Canary testnet** for development and testing. When you are ready to launch your application live, you will need to: -1. Set up your project on the **mainnet Enjin Platform Cloud**: https://platform.enjin.io/ -2. Update your application's API calls or SDKs to use the [**mainnet GraphQL API endpoints**](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md#mainnet) -::: - -Now you can explore more advanced integrations: - -- **On-Demand Minting:** Programmatically create *new* tokens directly into users' wallets based on real-time application events (e.g., crafting an item, completing a quest). This avoids pre-minting large supplies. - ➡️ [Learn about On-Demand Minting](/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md#option-b-using-the-enjin-api--sdks) -- **Enjin Wallet Daemon:** If your application requires frequent, automated minting or transferring without manual intervention, setting up the Wallet Daemon is highly recommended for secure signature automation. - ➡️ [Set up the Enjin Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) -- **Managed Wallets:** Simplify onboarding for users unfamiliar with crypto wallets. You can create and manage wallets for your users within the platform. Users can interact with blockchain features seamlessly, and later choose to export their items to their own self-custody Enjin Wallet if they wish. This significantly lowers the barrier to entry for Web2 users. - ➡️ [Explore Managed Wallets](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) \ No newline at end of file diff --git a/versioned_docs/version-2.x/01-getting-started/04-using-the-enjin-platform.md b/versioned_docs/version-2.x/01-getting-started/04-using-the-enjin-platform.md deleted file mode 100644 index a4fae39..0000000 --- a/versioned_docs/version-2.x/01-getting-started/04-using-the-enjin-platform.md +++ /dev/null @@ -1,589 +0,0 @@ ---- -title: "Using the Enjin Platform" -slug: "using-the-enjin-platform" -description: "Discover how the Enjin Platform simplifies blockchain asset management for developers, enabling seamless integration of tokenized assets in games and applications." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info What you'll need: -Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) to pay for and for s. -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). -::: - -## 1. Set Up an Account - -To get started, go ahead and create an account on the and **verify your email address.** In this tutorial, we'll be using the [Enjin Platform Cloud (Testnet)](https://platform.canary.enjin.io). - -:::info There are three versions of the Enjin Platform to choose from: -- The [Enjin Platform Cloud (Testnet)](https://platform.canary.enjin.io), connected to the Enjin . -- The [Enjin Platform Cloud (Mainnet)](http://platform.enjin.io/), connected to the Enjin . -- The [Enterprise On-Prem Enjin Platform](/05-enjin-platform/04-enterprise-on-prem/04-enterprise-on-prem.md) which you can run locally to connect to either network. -::: - -### Things you should know - -- is , which is why we recommend building your initial proof-of-concept on Enjin – the fast and free environment for testing Enjin's tools. -- Every "" request (i.e, minting a ) must be signed by a . - -## 2. Set Up an API Token - -Once your account is created, create a new API token by clicking the "**Create API Token**", which can be found on the [Account Settings](https://platform.canary.enjin.io/settings) page. - -![Creating an API token on the Enjin Platform](/img/getting-started/create-api-token.gif) - -Copy and save that token somewhere safe, you'll need that to automatically approve transactions using a . - -:::tip Note -You can always revoke the token and create another one in the future. -::: - -## 3. Set Up a Wallet - -Transactions can be approved in 2 different ways: - -- **Manually:** Using the [Enjin Wallet](https://enjin.io/products/wallet) / [Polkadot.js](https://polkadot.js.org/). -- **Automatically:** Using the [Enjin Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md). - -:::tip Start by manually approving transactions -If you're using the Enjin Platform for the first time, we recommend [Using Enjin Wallet / Polkadot.js](#a-using-enjin-wallet--polkadotjs) -Enjin wallet app is available on both [iOS](https://enj.in/ios-wallet) and [Android](https://enj.in/android-wallet). -::: - -### A. Using Enjin Wallet / Polkadot.js - -Connect your wallet by clicking the "**Connect Wallet**" button in the top right corner and follow the on-screen instructions. -The also offers gamers a user-friendly, secure, and rewarding experience, ensuring their continuous engagement with your game. - -![Connecting a wallet app on the Enjin Platform](/img/getting-started/connect-wallet-app.gif) - -### B. Using Daemon Wallet - -The Enjin Wallet Daemon can automatically sign requests for you. - -To download and set up wallet daemon, head over to the [Wallet Daemon Setup](/01-getting-started/06-using-wallet-daemon.md) page. -To ensure that only authorized requests are approved, the Wallet daemon needs to be configured with an Enjin Platform API Token. -Create an API Token on the [Account Settings](https://platform.canary.enjin.io/settings) page and set the Platform API Token in your Wallet Daemon configuration. - -Once your wallet daemon is connected, your Platform account is set up and ready to use! - -## 4. Create Platform Requests - -Now that the Platform account is set up, we can start creating platform requests. -In this example we will be creating a . - -**There are two ways to create Platform requests:** - -- [Using the Platform User Interface](#option-a-using-the-enjin-dashboard) -- [Using the GraphQL API](#option-b-using-the-enjin-api--sdks) - -:::info Which approach should I use? -Everything that can be done via the Platform's User Interface, can be done programmatically via the API or any of the Platform s. -If you are just starting out, we recommend using the Platform User Interface as it's more user friendly. -However, if you need to make a Platform request programmatically, you can do that via the / s. -::: - -### Option A. Using the Enjin Dashboard - -In the Platform menu, navigate to "**[Collections](https://platform.canary.enjin.io/collections)**". Then, click the "**[Create Collection](https://platform.canary.enjin.io/create/collection)**" button. - -![Create collection form on Enjin Platform](/img/getting-started/create-collection-form.png) - -From here, you can customize your collection's Mint Policy, Market Policy, Explicit Royalty Currencies (optional), and Attributes. - -Once you're satisfied with the options, click on the "**Create**" button at the bottom right corner to create the request. - -![Collection created banner on Enjin Platform](/img/getting-started/collection-created-banner.png) - -### Option B. Using the Enjin API & SDKs - -To create a Platform request programmatically, use the API, or one of the s. -This mutation will set up a new transaction that once finalized on-chain will contain the new collection id in the transaction . - -:::tip Test your requests in the GraphiQL Playground: -- [Testnet](https://platform.canary.enjin.io/graphiql) -- [Mainnet](https://platform.enjin.io/graphiql) -::: - -:::tip -If you are not using the playground, you need to add your API token to the headers to be authenticated, like so: -```json -"Authorization": "" -``` -::: - - - -```graphql -mutation CreateCollection { - CreateCollection(mintPolicy: { forceCollapsingSupply: false }) #Set to true to enforce collapsing supply mint policy - { - id - method - state - } -} -``` - - -```json -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation Infuse(\r\n $collection_id: BigInt!\r\n $token_id: EncodableTokenIdInput!\r\n $amount: BigInt!\r\n) {\r\n Infuse(collectionId: $collection_id, tokenId: $token_id, amount: $amount) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"forceCollapsingSupply":false}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the mutation -var createCollection = new CreateCollection() - .SetMintPolicy(new MintPolicy().SetForceCollapsingSupply(false)); //Set to true to enforce collapsing supply mint policy - -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -createCollection.Fragment(transactionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendCreateCollection(createCollection); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cplusplus -#include "EnjinPlatformSdk/CoreMutations.hpp" -#include -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Setup mutation data - shared_ptr mintPolicy = make_shared(); - mintPolicy->SetForceSingleMint(make_shared(false)); - - // Setup mutation - CreateCollection createCollection = CreateCollection(); - createCollection.SetMintPolicy(mintPolicy); - - // Define and assign the return data fragment to the mutation - shared_ptr transactionFragment = make_shared(); - transactionFragment - ->WithId() - .WithMethod() - .WithState(); - - createCollection.SetFragment(transactionFragment); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>>> futureResponse = SendCreateCollection(*client, createCollection); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& transaction = gqlResult->GetData()->GetResult(); - - std::cout << to_string(transaction->GetId().value()) << std::endl; - std::cout << ToString(transaction->GetMethod().value()) << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation CreateCollection($forceCollapsingSupply: Boolean) { - CreateCollection( - mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } - ) { - id - method - state - } - } - `, - variables: { - forceCollapsingSupply: false //Set to true to enforce collapsing supply mint policy - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation CreateCollection($forceCollapsingSupply: Boolean) { - CreateCollection( - mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } - ) { - id - method - state - } - } - `, - variables: { - forceCollapsingSupply: false //Set to true to enforce collapsing supply mint policy - } -}, { - headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation CreateCollection($forceCollapsingSupply: Boolean) { - CreateCollection( - mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } - ) { - id - method - state - } -} -''' - -variables = {'forceCollapsingSupply': False} #Set to true to enforce collapsing supply mint policy - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -A WebSocket event will also be fired so you can pick up the collection in real time by listening to the app channel on the WebSocket. - -## 5. Approve Platform Requests - -Once a Platform request is created, all that remains is to approve it, which will sign the transaction and broadcast it to the Enjin Blockchain - -Transaction requests are displayed in the "**[Transactions](https://platform.canary.enjin.io/transactions)**" menu. - -![Pending create collection txn on Enjin Platform](/img/getting-started/pending-create-collection-txn.png) - - -Since this transaction is a , you will need to sign the transaction using your Wallet. - -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. - -![Approving the create collection txn on Enjin Platform](/img/getting-started/approving-create-collection-txn.gif) - -And that's it! The transaction is then broadcasted to the Enjin Blockchain, and once approved, it will be included in a . - -## 6. Receive Transaction Information - -There are 3 ways to receive the transaction status and information: - -- [Receive Transaction Information Using the Platform User Interface](#receive-transaction-information-using-the-platform-user-interface) -- [Receive Transaction Information Using the Enjin API / SDKs](#receive-transaction-information-using-the-enjin-api--sdks) -- [Receive Transaction Information by Listening to Platform Websocket Events](#receive-transaction-information-by-listening-to-platform-websocket-events) - -### Receive Transaction Information Using the Platform User Interface - -To check the transaction status, head over to the [Transactions](https://platform.canary.enjin.io/transactions) page locate the transaction and click on the button to it's right to show more details. -Once the transactions approves on-chain, the transaction will get updated with the `FINALIZED` state and the transaction events will be displayed. -In the events, we can find useful information, such as the newly created collection ID. - -![How to find your newly created collection ID](/img/getting-started/find-newly-created-collection-id.gif) - -### Receive Transaction Information Using the Enjin API & SDKs - -To receive the transaction information programmatically, You can use the `GetTransaction` query and set the `id` to the ID received in the `CreateCollection` mutation. -But for this example, we're going to use the `GetPendingEvents` query, which returns all of the events emitted to your platform account. - -:::info This query might return items in multiple pages using Connections -To learn how to use GraphQL cursors for pagination, head to [Using the API --> Pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination). -::: - -**Query:** - - - -```graphql -query GetPendingEvents{ - GetPendingEvents{ - edges{ - node{ - name - data - } - } - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query": "query GetPendingEvents { GetPendingEvents { edges { node { name data } } } }"}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -var filterInputArray = new StringFilterInput[] -{ - new StringFilterInput() - .SetType(FilterType.And) - .SetFilter("collection;2000"), -}; - -// Setup the query -var getPendingEvents = new GetPendingEvents() - .SetChannelFilters(filterInputArray); - -// Define and assign the return data fragment to the query -var pendingEventsFragment = new ConnectionFragment() - .WithEdges(new EdgeFragment() - .WithNode(new PendingEventFragment() - .WithId() - .WithUuid() - .WithName() - .WithSent() - .WithChannels() - .WithData() - ) - ); - -getPendingEvents.Fragment(pendingEventsFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendGetPendingEvents(getPendingEvents); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cplusplus - -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query GetPendingEvents{ - GetPendingEvents{ - edges{ - node{ - name - data - } - } - } - } - `, - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - query GetPendingEvents{ - GetPendingEvents{ - edges{ - node{ - name - data - } - } - } - } - `, -}, { - headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' - query GetPendingEvents{ - GetPendingEvents{ - edges{ - node{ - name - data - } - } - } - } -''' - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -**Response:** - -```json -{ - "data": { - "GetPendingEvents": { - "edges": [ - { - "node": { - "name": "platform:transaction-created", - "data": { - "id": 1648, - "state": "PENDING", - "method": "CreateCollection", - "idempotencyKey": "00e32324-acfa-4ddb-8b7d-6dd8d0b4f694" - } - } - }, - { - "node": { - "name": "platform:transaction-updated", - "data": { - "id": 1648, - "state": "BROADCAST", - "method": "CreateCollection", - "result": null, - "transactionId": null, - "idempotencyKey": "00e32324-acfa-4ddb-8b7d-6dd8d0b4f694", - "transactionHash": "0xeb2510094c53317e19ca188b5ba8a21b81f36f4bc1be09e079b46ee7b9bce754" - } - } - }, - { - "node": { - "name": "platform:collection-created", - "data": { - "owner": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", - "collectionId": "38224", - "idempotencyKey": "00e32324-acfa-4ddb-8b7d-6dd8d0b4f694" - } - } - }, - { - "node": { - "name": "platform:transaction-updated", - "data": { - "id": 1648, - "state": "FINALIZED", - "method": "CreateCollection", - "result": "EXTRINSIC_SUCCESS", - "transactionId": "1404095-2", - "idempotencyKey": "00e32324-acfa-4ddb-8b7d-6dd8d0b4f694", - "transactionHash": "0xeb2510094c53317e19ca188b5ba8a21b81f36f4bc1be09e079b46ee7b9bce754" - } - ] - } - } -} -``` - -The first event with `PENDING` state was emitted when the `CreateCollection` platform request was created. -The second event with `BROADCAST` state was emitted when the transaction was signed and broadcasted to the Enjin Blockchain. -The third event was emitted when the collection was created on-chain, and has the `collectionId` in it's data. -The forth and final event with `FINALIZED` state was emitted when the platform request was finalized. - -:::info Note -For the full list of platform states, check the [Enjin API --> Important Arguments](/03-api-reference/04-important-arguments.md#state) page. -::: - -:::tip Acknowledging Events -To remove the pending events from the queue after fetching them, set the `acknowledgeEvents` parameter to true -::: - -### Receive Transaction Information by Listening to Platform Websocket Events - -You can also subscribe for events via Websocket. for more information, check out the [Websocket Events](/05-enjin-platform/03-working-with-events.md) - -:::tip What's Next? -Learn how to [Use the API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md). -::: diff --git a/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md b/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md deleted file mode 100644 index 39619d2..0000000 --- a/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md +++ /dev/null @@ -1,792 +0,0 @@ ---- -title: "How to Use GraphQL" -slug: "../how-to-use-graphql" ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -This guide is intended for users who are new to GraphQL. If you're already familiar with how GraphQL works - how to structure queries, use variables, and interact with GraphQL endpoints—you can skip this page and proceed directly to guides. - -For everyone else, this page will walk you through the essentials of working with GraphQL so you can confidently interact with our API. - -## Understanding GraphQL vs. RESTful API Calls - -To illustrate the differences between GraphQL and RESTful API calls, let's consider an example of creating a token. - -### RESTful API Example - -In a typical RESTful API, you might have an endpoint like `/createToken` with a POST request that includes the necessary data in the request body. This might look something like: - -```http -POST https://platform.canary.enjin.io/api/createToken -Content-Type: application/json - -{ - "recipient": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", - "collectionId": 2406, - "tokenId": 0, - "initialSupply": 1, - "cap": "INFINITE" -} -``` - -And would traditionally return all of the available data related to the call, like so: - -```json -{ - "data": { - "CreateToken": { - "id": "14060", - "transactionId": "null", - "transactionHash": "null", - "method": "CreateToken", - "state": "PENDING", - "encodedData": "...", - "wallet": { - "account": { - "publicKey": "...", - "address": "..." - } - } - } - } -} -``` - -### GraphQL API Example - -In contrast, GraphQL uses a single endpoint for all operations. Instead of defining the operation in the URL and HTTP method, the desired action (query or mutation), its parameters, and the response fields are specified in the request payload. - -In GraphQL: - -- `Queries` are used to retrieve data, similar to a `GET` request in REST. -- `Mutations` are used to create or modify data, similar to `POST`, `PUT`, or `DELETE` requests in REST." - -Here's how the same RESTful request would like like in GraphQL, sent as an HTTP post request to the Canary Platform Cloud GraphQL endpoint https://platform.canary.enjin.io/graphql: - -```graphql -mutation CreateToken { - CreateToken( - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", - collectionId: 2406, - params: { - tokenId: { integer: 0 }, - initialSupply: 1, - cap: { type: INFINITE } - } - ) { - id - method - state - } -} -``` - -With GraphQL, the client controls what data is returned by explicitly specifying the fields in the query (In this example - the `id`, `method`, and `state`). This enables fine-grained control over the response, minimizing unnecessary data transfer. - -GraphQL's ability to query multiple resources in a single request, its strong type system, and its flexibility in querying data make it a powerful tool for interacting with the Enjin Blockchain, providing a seamless experience for developers building with NFTs. - -## GraphiQL Playground - -The serves three purposes: - -1. **Explore** available operations. -2. **Write** operations using an intuitive interface. -3. **View** responses and test your operations before coding them. - -![A breakdown of the GraphiQL Playground](/img/getting-started/graphiql-explorer-breakdown.png) - -### Step 1: Open GraphiQL Playground - -Choose between [Testnet](https://platform.canary.enjin.io/graphiql) or [Mainnet](https://platform.enjin.io/graphiql). - -Explore "[Core Operations](https://platform.canary.enjin.io/graphiql)" for core operations like managing tokens, wallets, users, and transactions. - -:::info GraphiQL Playground: -Please be aware that the provided links below are specifically for the GraphiQL Playground, which is a graphical user interface designed for exploring and interacting with the Enjin Platform GraphQL APIs directly from a web browser. -If you wish to execute GraphQL requests programmatically from your local environment or a cloud-based application, you will need to utilize the actual GraphQL endpoint URLs. These can be found [here](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md#overview-of-graphql-endpoints) -#### Testnet: -- [Core Operations](https://platform.canary.enjin.io/graphiql) -- [Marketplace](https://platform.canary.enjin.io/graphiql/marketplace) -- [Beam](https://platform.canary.enjin.io/graphiql/beam) -- [Fuel Tanks](https://platform.canary.enjin.io/graphiql/fuel-tanks) -#### Mainnet: -- [Core Operations](https://platform.enjin.io/graphiql) -- [Marketplace](https://platform.enjin.io/graphiql/marketplace) -- [Beam](https://platform.enjin.io/graphiql/beam) -- [Fuel Tanks](https://platform.enjin.io/graphiql/fuel-tanks) -::: - -### Step 2: Open the explorer - -Click the `Show GraphQL Explorer` button. - -Here, you can view a complete list of available queries and mutations, along with the associated fields and arguments for each of them. - -

- Example of using the graphiql playground query builder -

- -### Step3: Add a query or mutation to start building your operation - -Add a `query` or `mutation` into the "write" panel, and you'll see the list of available options in the explorer update to suit the operation type. - -

- Example of switching to mutation in the graphiql playground query builder -

- -### Step 4: Select which operations and datapoints you require - -Click the required data points to automatically add them to the write column. - -This feature eliminates any guesswork when structuring your operations and helps you make the most of the Enjin Platform Schema. - -

- Example of building a mutation in the graphiql playground query builder -

- -### Step 5: Click the play button to execute your operation - -To execute your operation and view the response, simply click the `Execute Query` button. - -You can execute any on-chain operation here. You can even mint tokens and view wallet data on [Mainnet](https://platform.enjin.io/graphiql). - -

- Executing a query in the graphiql playground query builder -

- -## GraphQL Operation Structures - -The Enjin API features an intricately crafted that is finely tuned to empower the development of scalable and high-speed blockchain-based games, ensuring an immersive experience for players. - -To interface with it directly, you will need to understand how GraphQL operations are structured. - -### Operation Name - -The Operation Name such as `GetTokenInfo` is a user-defined identifier for a specific operation. - -It allows you to organize and distinguish different operations within a single request, making it easier to handle responses and manage complex queries or mutations efficiently. - - - -```graphql -query GetTokenInfo { #'GetTokenInfo' is the Operation Name that distinguishes this operation from others - GetToken( - collectionId: 7153 - tokenId: { integer: 10 } - ) { - supply - } -} -``` - - -```json -{ - "data": { - "GetToken": { - "supply": "1000" - } - } -} -``` - - - -### Fields - -Fields serve as a means to precisely specify the data you want to retrieve in the response of an operation. - -Fields enable you to fine-tune the granularity of data retrieval, ensuring you receive precisely what you need. - -When crafting Queries and Mutations, the Enjin Platform defines what fields are available for you to request. - -For instance, when using the `GetToken` query, you can request the `supply` field, which corresponds to a specific piece of information stored on the blockchain. - - - -```graphql -query { - GetToken( - collectionId: 7153 - tokenId: { integer: 10 } - ) { - supply # supply is a Field that returns the token's supply - } -} -``` - - -```json -{ - "data": { - "GetToken": { - "supply": "1000" - } - } -} -``` - - - -### Arguments - -You can pass data into an operation by using arguments. - -Most Queries and Mutations define a set of input data you can use, for example in the `GetToken` query you can pass in a `collectionId` and a `tokenId` argument. - - - -```graphql -query { - GetToken( - collectionId: 7153 #collectionId is an argument defining which collection to pull data from - tokenId: { integer: 10 } #tokenId is an argument defining which token to pull data from - ) { - supply - } -} -``` - - -```json -{ - "data": { - "GetToken": { - "supply": "1000" - } - } -} -``` - - - -Sometimes arguments will be required, sometimes they will be optional, and some operations may not take any arguments at all. - -The API will tell you if you try to run an operation without a required argument. - -### Aliases - -The alias, such as `TokenInfo` and `CurrentSupply` in the queries provided below, serve as a custom labels for specific fields within the operation. - -Aliases allow you to rename the field in the response, making the data more intuitive and organized, especially when dealing with multiple fields or nested queries within a GraphQL request. - -#### Operation Alias: - -Usually used in batch operations, it allows you to provide a clear and descriptive name for each operations. - -You might use it to distinguish between multiple similar queries or to provide context for the purpose of the query. - - - -```graphql -query GetTokenInfo { - TokenInfo: GetToken( #TokenInfo is the ALIAS that labels the field - collectionId: 7153 - tokenId: { integer: 10 } - ) { - supply - } -} -``` - - -```json -{ - "data": { - "TokenInfo": { - "supply": "1000" - } - } -} -``` - - - -#### Field Alias: - -This option is useful when you want to label a specific field within the response. - -It allows you to rename individual fields in the response for clarity or to avoid naming conflicts. - - - -```graphql -query GetTokenInfo { - GetToken( - collectionId: 7153 - tokenId: { integer: 10 } - ) { - CurrentSupply: supply - } -} -``` - - -```json -{ - "data": { - "GetToken": { - "CurrentSupply": "1000" - } - } -} -``` - - - -### Variables - -Various code blocks in the Enjin Documentation use variables instead of hardcoding arguments. This allows the dynamic insertion of data as you run mutations and queries. - -For example, when a player picks up an item, your game needs to fetch the token information using the token's ID. To dynamically insert the token ID to the query, we should use a variable. - -That way, we will have one "template" for token fetching query, and the game will decide which token to fetch based on the player actions. - -To work with variables in GraphQL, we first need to declare the `$variableName`, as the variables accepted by the operation. Our operations has two arguments - Collection ID and Token ID, so we'll name the variables `$collection_id` and `$token_id` respectively. Both arguments are of type `BigInt`, so the variable declaration will look like that: - -``` -query GetTokenInfo($collection_id: BigInt! $token_id: BigInt) { -``` - -:::tip -Note the exclamation mark (!) after the first variable declaration type BigInt. This represents a _non-nullable_ variable (essentially a _required field_). -::: -We also need to replace the static value in the operation with the `$variableName` - -``` -GetToken( - collectionId: $collection_id //Collection ID 7153 was hardcoded here - tokenId: { integer: $token_id } //Token ID 10 was hardcoded here -) -``` - -And the final step is to pass `variableName: value` in a separate, transport-specific (usually JSON) variables dictionary: - -```json -{ - "collection_id": 7153, - "token_id": 10 -} -``` - -Here's the final operation code: - -**Query:** - -```graphql -query GetTokenInfo($collection_id: BigInt! $token_id: BigInt) { - GetToken( - collectionId: $collection_id - tokenId: { integer: $token_id } - ) { - supply - } -} -``` - -**Variables:** - -```json -{ - "collection_id": 37108, - "token_id": 0 -} -``` - - -

- Example of converting query args into variables -

- -### Types - -Types in GraphQL serve as blueprints for defining the shape and composition of the data available through the API, encompassing a collection of fields representing specific pieces of information. - -These structures lay the foundation for shaping queries and mutations, ensuring precise data retrieval and manipulation. - -#### Query: - -A `query` is used to retrieve data from the Enjin Platform. - -In this example, we're querying for information about a specific token. - - - -```graphql -query GetTokenInfo { - GetToken( - collectionId: 7153 - tokenId: { integer: 10 } - ) { - supply # Returns the current supply of the token - } -} -``` - - -```json -{ - "data": { - "TokenInfo": { - "supply": "1000" - } - } -} -``` - - - -The `GetToken` query retrieves token information. - -We specify the `collectionId` and `tokenId` as parameters to identify the token we want. - -The fields `tokenId` and supply indicate the data we want to receive in the response. - -#### Mutation: - -A `mutation` is used to modify data on the Enjin Platform. For example, you can use a mutation to update a user's profile information. - -In this example, we're minting a new token. - - -```graphql -mutation MintToken { - MintToken( - recipient: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" - collectionId: "7154" - params: { - amount: 1 - tokenId: { integer: 6533 } - } - ) { - id # Returns the transaction ID - } -} -``` - - -```json -{ - "data": { - "MintToken": { - "id": "1234" // request ID - } - } -} -``` - - - -The `MintToken` mutation mints a new token. - -We provide the necessary arguments, such as `recipient`, `collectionId`, and `params`, to define the minting process. - -The field `id` indicate the data we want to receive in the response. - -#### Batch Query: - -A batch `query` allows you to send multiple mutation operations in a single request to reduce network overhead. - -Each query have its own set of parameters and return data. - - - -```graphql -query BatchGetTokenInfo { - TokenInfo1: GetToken( - collectionId: 7153 # Specifies the collection ID for the first token - tokenId: { integer: 10 } # Specifies the unique identifier of the first token - ) { - supply # Returns the current supply of the first token - } - TokenInfo2: GetToken( - collectionId: 4553 # Specifies the collection ID for the second token - tokenId: { integer: 55 } # Specifies the unique identifier of the second token - ) { - supply # Returns the current supply of the second token - } -} -``` - - -```json -{ - "data": { - "TokenInfo1": { - "supply": 100 // Current supply of the first token - }, - "TokenInfo2": { - "supply": 200 // Current supply of the second token - } - } -} -``` - - - -In this batch query, we retrieve information for two different tokens with distinct `collectionId` and `tokenId`. - -Each token's data is aliased using `TokenInfo1` and `TokenInfo2`. - -#### Batch Mutation - -A batch `mutation` allows you to send multiple mutation operations in a single request to reduce network overhead. - -Each mutation can have its own set of parameters and return data. - - - -```graphql -mutation { - MintToken1: MintToken( - recipient: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" # Specifies the recipient's address for the first MintToken operation - collectionId: "7154" # Specifies the collection ID for the first MintToken operation - params: { - amount: 1 # Specifies the amount to mint for the first MintToken operation - tokenId: { integer: 6533 } # Specifies the unique identifier of the first token to mint - } - ) { - id # Returns the request ID for the first MintToken operation - } - MintToken2: MintToken( - recipient: "0x1234567890abcdef" # Specifies the recipient's address for the second MintToken operation - collectionId: "7155" # Specifies the collection ID for the second MintToken operation - params: { - amount: 2 # Specifies the amount to mint for the second MintToken operation - tokenId: { integer: 6534 } # Specifies the unique identifier of the second token to mint - } - ) { - id # Returns the request ID for the second MintToken operation - } -} -``` - - -```json -{ - "data": { - "MintToken1": { - "id": "1234" // request ID for the first MintToken operation - }, - "MintToken2": { - "id": "1235" // request ID for the second MintToken operation - } - } -} -``` - - - -In this batch mutation, we have two separate `MintToken` mutations, labeled using the `MintToken1` and `MintToken2` aliases. - -Both mutations are minting two different tokens into two separate addresses. - -### Pagination - -Some GraphQL queries returns results in multiple pages. In GraphQL, these are called "Connections", and each of the elements in the connection is called "Edge". - -For example, the [GetWallet query](https://studio.apollographql.com/public/EnjinPlatform/variant/core/schema/reference/objects/Query?query=GetWallet) returns an object of type [Wallet](https://studio.apollographql.com/public/EnjinPlatform/variant/core/schema/reference/objects/Wallet). Wallet objects contains tokenAccount which is of type [TokenAccountConnection](https://studio.apollographql.com/public/EnjinPlatform/variant/core/schema/reference/objects/TokenAccountConnection). Since this object is of type 'connection', it uses pagination. - -By default, if you don't specify which page to query, the first page will be returned. -To check if the response contains another page for a certain object, use the pageInfo.hasNextPage property from the connection. - - - -```graphql -query FetchingWalletTokens{ - GetWallet(account: "cxKnfok66R8BAuzGcypxqirYcqW7E9Spn4z5UZSVRuUHAcrTQ"){ - tokenAccounts{ - pageInfo{ - hasNextPage #Returns whether there's a next page or not - } - edges{ - node{ - token{ - tokenId - } - balance - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetWallet": { - "tokenAccounts": { - "pageInfo": { - "hasNextPage": true // Indicates that there is another page available. - }, - "edges": [ - { - "node": { - "token": { - "tokenId": "5" - }, - "balance": "1" - } - }, - {...}, - {...} - ] - } - } - } -} -``` - - - -By default, this connection returns 15 edges (tokens) per page. - -:::tip -The amount of edges per page can be adjusted by using slicing. -::: - -To navigate to the next page, use pagination combined with the edge cursor. -First, add the cursor field to the query response. (Alternatively, you can add the pageInfo.endCursor field to fetch the cursor of the last edge in the connection) - - - -```graphql -query FetchingWalletTokens{ - GetWallet(account: "cxKnfok66R8BAuzGcypxqirYcqW7E9Spn4z5UZSVRuUHAcrTQ"){ - tokenAccounts{ - pageInfo{ - hasNextPage - endCursor #Returns The cursor of the last tokenAccount in the current page - } - edges{ - cursor #Returns The tokenAccount cursor - node{ - token{ - tokenId - } - balance - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetWallet": { - "tokenAccounts": { - "pageInfo": { - "hasNextPage": true, - "endCursor": "eyJpZCI6Mjk0NzQsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0" // The cursor of the last tokenAccount in the current page - }, - "edges": [ - { - "cursor": "eyJpZCI6MzU3NCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", // The tokenAccount cursor - "node": { - "token": { - "tokenId": "5" - }, - "balance": "1" - } - }, - {...}, - {...}, - { - "cursor": "eyJpZCI6Mjk0NzQsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", // The tokenAccount cursor - "node": { - "token": { - "tokenId": "11" - }, - "balance": "1" - } - } - ] - } - } - } -} -``` - - - -Now we have the cursor of the last edge of the current page. -To navigate to the next page, add the `after` parameter to the `tokenAccounts` connection, and insert the cursor of the last edge of the current page. - - - -```graphql -query FetchingWalletTokens{ - GetWallet(account: "cxKnfok66R8BAuzGcypxqirYcqW7E9Spn4z5UZSVRuUHAcrTQ"){ - tokenAccounts(after: "eyJpZCI6Mjk0NzQsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0"){ #Indicates that we want to fetch tokenAccounts after the specified cursor. - pageInfo{ - hasNextPage - endCursor - } - edges{ - cursor - node{ - token{ - tokenId - } - balance - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetWallet": { - "tokenAccounts": { - "pageInfo": { - "hasNextPage": true, - "endCursor": "eyJpZCI6NTE2MzgsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0" - }, - "edges": [ - { - "cursor": "eyJpZCI6Mzc2MTYsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "token": { - "tokenId": "9" - }, - "balance": "1" - } - }, - {...}, - {...}, - { - "cursor": "eyJpZCI6NTE2MzgsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "token": { - "tokenId": "1" - }, - "balance": "1" - } - } - ] - } - } - } -} -``` - - - -To get all edges in the connection, continue navigating to the next page with a loop. -You'll know that you reached the last page when the `hasNextPage` returns `false`. - -*** - -With this understanding, you're ready to create any GraphQL requests the Enjin API has to offer. diff --git a/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/05-using-enjin-api.md b/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/05-using-enjin-api.md deleted file mode 100644 index 471e156..0000000 --- a/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/05-using-enjin-api.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: "Using the Enjin API" -slug: "../using-the-enjin-api" -description: "Explore the Enjin API and learn how to easily interact with the Enjin blockchain, mint tokens, and manage digital assets programmatically." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; - -## Introduction to the Enjin API - -The Enjin API is a set of programmatic interfaces that allow developers to interact with the Enjin Platform from their own applications. Built using , it enables clients to request exactly the data they need in a flexible and efficient way. Unlike traditional RESTful APIs, which require multiple endpoints for different data requirements, GraphQL enables more efficient data retrieval through a single endpoint. This flexibility reduces the number of network requests and optimizes performance, making it particularly advantageous for applications that require efficient data handling, such as blockchain and NFT platforms. - -:::info What you'll need: -- Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) to pay for . -- You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -::: - -:::tip New to GraphQL? -If you're more familiar with REST APIs, it's important to understand that our API uses GraphQL - a flexible and powerful query language for APIs. GraphQL works quite differently from REST, and knowing how to structure your queries and handle responses is essential for successful integration.\ -We recommend reviewing our [How to Use GraphQL guide](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md) to get started quickly and effectively. -::: -## Authentication - -Access tokens assume a pivotal role in facilitating your application's interaction with the Enjin Platform API. -These tokens serve a dual purpose: - -- They grant your application access to the platform. -- They allow for automatic request approval and signing using the Wallet Daemon. - -To make authenticated calls to the Enjin Platform: - -1. login or create an account in the [Testnet Enjin Platform Cloud](https://platform.canary.enjin.io). -2. Create an API token if you haven't already. -3. When making HTTP requests to the GraphQL endpoints, include your API token in the request headers using the `Authorization` field: - -``` -"Authorization": "" -``` - -4. _(Optional)_ To automate requests made from the Enjin Platform using a wallet daemon, you need to configure your daemon with the API token. - For more details head over to [Using the Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) - -## Endpoints & Queries - -Every data transfer strictly adheres to the HTTP/1.1 standard, with HTTPS encryption mandatory for all endpoints to ensure secure communication. - -The Enjin API primarily utilizes GraphQL, meaning you'll be sending POST HTTP requests to specific endpoints with your GraphQL queries and mutations. This allows you to interact with the Enjin blockchain in a flexible and efficient manner. - -:::tip SDKs for Streamlined Development -For developers looking for a more integrated experience, Enjin also provides official **Software Development Kits (SDKs)** for various programming languages. These SDKs wrap the underlying GraphQL API, offering convenient methods and abstractions to streamline development. You can find more information and links to the available SDKs on our [Software Development Kits (SDKs) page](/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md). -::: - -### Overview of GraphQL Endpoints - -The Enjin API is structured around four distinct GraphQL endpoints, each designed to handle specific sets of queries and mutations. This segmentation allows for efficient and organized interactions tailored to different functional areas within the Enjin ecosystem: - -1. **Core Operations:** This endpoint handles fundamental blockchain operations such as creating collections, minting and transferring s, freezing, and burning s . It provides the essential tools for managing and interacting with s on the Enjin Blockchain. - -2. **Marketplace:** Focused on marketplace activities, this endpoint encompasses operations like listing tokens for sale, purchasing tokens, and bidding on auctions. It facilitates seamless transactions and interactions within the Enjin Marketplace. - -3. **Beam:** The Beam endpoint manages operations related to the Beam system, which is used for distributing tokens claimable via QR codes. This feature is particularly useful for promotional activities and engaging user experiences. - -4. **Fuel Tanks:** This endpoint is dedicated to the Fuel Tank system, which allows for subsidizing transaction fees. It supports operations that manage and configure fuel tanks, enabling cost-effective transactions on the blockchain. - -GraphQL queries and mutations are executed by sending POST HTTP requests to the corresponding endpoint: - -:::note GraphQL Endpoints -### Testnet: -- **Core Operations** `https://platform.canary.enjin.io/graphql` -- **Marketplace** `https://platform.canary.enjin.io/graphql/marketplace` -- **Beam** `https://platform.canary.enjin.io/graphql/beam` -- **Fuel Tanks** `https://platform.canary.enjin.io/graphql/fuel-tanks` -### Mainnet: -- **Core Operations** `https://platform.enjin.io/graphql` -- **Marketplace** `https://platform.enjin.io/graphql/marketplace` -- **Beam** `https://platform.enjin.io/graphql/beam` -- **Fuel Tanks** `https://platform.enjin.io/graphql/fuel-tanks` -::: -Here is an example of an HTTP cURL post request to create a collection on the Canary blockchain, with the API Token provided: - -``` -curl --location --request POST 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: Insert your API token here' \ --d '{"query":"mutation CreateCollection($forceCollapsingSupply: Boolean) {\r\n CreateCollection(\r\n mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"forceCollapsingSupply":false}}' -``` - -## API Reference - -For a comprehensive guide on exploring and utilizing GraphQL queries and mutations in the Enjin API, please refer to our [API Reference](/03-api-reference/03-api-reference.md). - -*** - -:::tip What's next? -- New to GraphQL? Learn how to structure queries and mutations in our [How to Use GraphQL guide](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md). -- To automate signing requests, continue to the [Using the Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) page. -- Or, If you're ready to start building... [Create a Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md). diff --git a/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/_category_.json b/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/_category_.json deleted file mode 100644 index e44d142..0000000 --- a/versioned_docs/version-2.x/01-getting-started/05-using-enjin-api/_category_.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "label": "Using the Enjin API" -} diff --git a/versioned_docs/version-2.x/01-getting-started/06-using-wallet-daemon.md b/versioned_docs/version-2.x/01-getting-started/06-using-wallet-daemon.md deleted file mode 100644 index 49d1102..0000000 --- a/versioned_docs/version-2.x/01-getting-started/06-using-wallet-daemon.md +++ /dev/null @@ -1,172 +0,0 @@ ---- -title: "Using the Wallet Daemon" -slug: "using-wallet-daemon" -description: "Get started with the Enjin Wallet Daemon, an automated tool for managing blockchain transactions and assets securely and efficiently." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -The Enjin Wallet Daemon provides a streamlined process for signing blockchain transactions, enabling seamless and efficient transactions between your game and the blockchain. This tool creates a persistent bridge between your game and the blockchain, ensuring a fluid gaming experience for players. - -In the Enjin Platform context, the Wallet Daemon is a utility tool that manages a blockchain wallet address associated with an Enjin Blockchain account. When a transaction is initiated on the Enjin Platform, the Wallet Daemon receives the transaction, signs it, and sends it back to the platform. This ensures secure and efficient transaction processing for Enjin Platform users. - -![A diagram of the Enjin Platform ](/img/getting-started/enjin-platform-diagram.png) - -The diagram above provides insight into the interaction between the Enjin Wallet Daemon and the Enjin Platform. This illustrates how the Wallet Daemon can communicate with the API in both directions, automatically signing and broadcasting transactions to the blockchain. This helps developers better understand how the two components work together seamlessly to provide a streamlined experience. - -## Wallet Daemon Events - -![A diagram of the wallet daemon](/img/getting-started/wallet-daemon-diagram.png) - -The diagram above depicts the various stages involved in creating a new collection on the Enjin Matrixchain via the Enjin Platform API, with a particular emphasis on the Wallet Daemon events. - -1. **Establish a WebSocket connection -** Connect to a WebSocket server to subscribe to channels that receive events. -2. **Subscribe to a WebSocket channel -** Subscribe to the channel associated with your wallet to receive real-time events. -3. **Send a mutation -** Send a "CreateCollection" mutation to the Enjin Platform API to create a new collection on the Enjin Matrixchain. The API will encode the transaction that the wallet daemon needs to sign and save it in the Enjin Platform Database. -4. **Wallet daemon requests pending transactions -** The wallet daemon repeatedly asks the Enjin Platform API if there are any transactions to sign to prevent incoming connections to the wallet daemon, which holds your . -5. **Sign and broadcast the transaction -** After receiving the pending transaction, the wallet daemon signs it with your private key and broadcasts it to the Enjin Matrixchain. -6. **Enjin Matrixchain processes the transaction -** If everything is correct and valid, the Matrixchain successfully processes the extrinsic sent by the wallet daemon, and the new collection is created. -7. **Enjin Platform API monitors the chain -** The Enjin Platform API continually watches the chain via a WebSocket connection to detect any activity. -8. **Save the data to the database -** The Enjin Platform API stores the new collection data in its own database, allowing you to query it as desired. -9. **Emit an event -** The Enjin Platform API sends an event to the WebSocket channels, providing you with the opportunity to take appropriate actions if you're subscribed to the relevant channel. - -*** - -:::info There are two approaches for running the Wallet Daemon: -- Using the [Wallet Daemon Executable](#wallet-daemon-executable). -- Setting up [Wallet Daemon via Docker](#setting-up-wallet-daemon-via-docker). -::: - -:::tip Choosing the Right Approach for Running the Wallet Daemon -There are two ways to run the Wallet Daemon, each suitable for different user profiles and use cases: -1. **Executable Approach:** This method involves running a simple executable, configuring it via a user interface, and clicking 'Run' to start it. It is the most straightforward approach and is recommended for beginners or those who are not developers and for development environments / personal use. Learn how to set it up in our [Setting up Wallet Daemon using Executable](#wallet-daemon-executable) guide. -2. **Docker Approach:** This method involves cloning a GitHub repository and modifying the configuration files. It offers more flexibility and control, making it suitable for developers and for production use. Learn how to set it up in our [Setting up Wallet Daemon using Docker](#setting-up-wallet-daemon-via-docker) guide. - -Choose the approach that best fits your technical proficiency and the needs of your project. -::: - -## Wallet Daemon Executable - -![The wallet daemon executable app](/img/getting-started/wallet-daemon-executable-welcome.png) - -Setting up the Wallet Daemon Executable is a straight forward process. -Download the latest version of the Wallet Daemon from [GitHub](https://github.com/enjin/wallet-daemon-ui/releases), extract it, and run the `enjin_wallet_daemon.exe` executable file. -Follow the on-screen instructions to set it up and make sure to insert your Enjin Platform API Token from your [account settings page](https://platform.canary.enjin.io/settings). - -![Configuring the Wallet Daemon Executable with Enjin Platform](/img/getting-started/daemon-exec-overview.gif) - -## Setting up Wallet Daemon via Docker - -:::note Code repository -The code repository can be found at https://github.com/enjin/wallet-daemon/ -::: - -It is recommended that the Enjin Wallet Daemon is installed and ran in isolation. This means running it on a dedicated server. The daemon itself is incredibly light-weight and does not require any extensive resources. - -:::tip Have Docker installed? -If you don't have Docker installed, you may install either the docker engine or docker desktop, though if you have never used docker before we recommend using docker desktop: https://www.docker.com/products/docker-desktop -::: - -:::warning Action Required: [Sentosa Upgrade](https://enjin.io/blog/enjin-blockchain-sentosa-upgrade) -To ensure uninterrupted service and transaction signing capabilities, users must upgrade their Wallet Daemon to v2.1.1 or greater before the [Sentosa blockchain upgrade](https://enjin.io/blog/enjin-blockchain-sentosa-upgrade). - -> Canary Testnet: The Sentosa upgrade is already live (since October 27, 2025). If you are using the Canary Wallet Daemon to sign transactions, you must update immediately to v2.1.1+ or transactions will fail. - -> Mainnet: The upgrade is scheduled for December 8, 2025 (approx. 16:30 UTC, at Relaychain block 13229687 and Matrixchain block 7734855). While older versions will work until this date, it is highly recommended to upgrade to the latest version beforehand. - -Instructions on upgrading the wallet-daemon to the latest version can be found [here](#updating-the-daemon-to-the-latest-version) -::: - -To download the Enjin Daemon, clone the `enjin/wallet-daemon` repository from GitHub by running this command in the Terminal: - -```bash -git clone https://github.com/enjin/wallet-daemon.git -``` - -### Configure Daemon - -You will want to update the `.env` file with the `PLATFORM_KEY` (which is an API Token generated within the Settings page of the Enjin Platform Cloud). In addition to this, you'll want to specify a unique `KEY_PASS`. - -:::danger Important note -The `KEY_PASS` is immutable. It is directly used to derive the wallet private key. Choose something unique and make sure to backup this `KEY_PASS` in a secure manner. -::: - -The final configuration is to update the `config.json` file to communicate with either the Enjin Blockchain (mainnet) or Canary Blockchain (testnet). You can do this by updating the node property in the JSON file. You can find an example of both networks below. - - - -``` -{ - "node": "wss://rpc.matrix.blockchain.enjin.io:443", - "relay_node": "wss://rpc.relay.blockchain.enjin.io:443", - "api": "https://platform.enjin.io/graphql", - "master_key": "store" -} -``` - - -``` -{ - "node": "wss://rpc.matrix.canary.enjin.io:443", - "relay_node": "wss://rpc.relay.canary.enjin.io:443", - "api": "https://platform.canary.enjin.io/graphql", - "master_key": "store" -} -``` - - - -### Starting the Daemon - -Finally, you can spin up the daemon using the following command from your local `wallet-daemon` directory: - -`docker compose up -d daemon` - -On the initial launch, a 12-words mnemonic seed will be created in the `/store` folder, with an additional encryption using the secret pass provided in the `KEY_PASS` environment variable. If you ever need to import this wallet into any wallet app, the derivation path is as follows: -`///` - -### Updating the Daemon to the latest version - -To upgrade your Docker installation of the Wallet Daemon to the latest version, follow these steps from within your local `wallet-daemon` directory: - -1. **Pull the latest code from the repository:** - ```bash - git pull - ``` -2. **Stop the currently running container:** - ```bash - docker compose stop daemon - ``` -3. **Rebuild the Docker image to incorporate the updates:** - ```bash - docker compose build --no-cache - ``` -4. **Start the daemon again:** - ```bash - docker compose up -d daemon - ``` - -### Importing Wallet Daemon From Existing Seed - -:::info Wallet Daemon Encryption -Wallet Daemon may be encrypted with a password specified in the `KEY_PASS` env var located in the `.env` file. -If your existing wallet is encrypted with a password, make sure to update your `KEY_PASS` var accordingly. -If your existing wallet is not encrypted, leave the `KEY_PASS` var empty. -::: - -Follow the steps below to set up your wallet daemon from an existing seed: - -1. Find your wallet's public key. You can convert your wallet's account address to it's public key with the [Account Format Transform](https://matrix.subscan.io/tools/format_transform) tool. - We'll use wallet address `efRP7f5aFWWobNiNxcWGNxhY1RdRXZ4kScvwuFdD4bsBHEUZW` as an example. It's public key is `0x62c75d8f81e05794cd0b703cf07b7ea3196840eaac4e300cb968fdd266882e02`. -2. Remove the 0x from the public key from step #1. - For our example, it's `62c75d8f81e05794cd0b703cf07b7ea3196840eaac4e300cb968fdd266882e02` -3. Inside your local `wallet-daemon/store` folder, create a file with the name `73723235`. - For our example, we've created the file `/store/7372323562c75d8f81e05794cd0b703cf07b7ea3196840eaac4e300cb968fdd266882e02` -4. Inside the new file created on step #3, insert your wallet's mnemonic seed wrapped with double quotes. - Example: `"earn meat maid rotate ..."` -5. Rebuild your docker container for the changes to take effect: - `docker compose build` -6. Run the daemon again: - `docker compose up daemon` diff --git a/versioned_docs/version-2.x/01-getting-started/_category_.json b/versioned_docs/version-2.x/01-getting-started/_category_.json deleted file mode 100644 index 1ff6c67..0000000 --- a/versioned_docs/version-2.x/01-getting-started/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "Getting Started", - "link": { - "type": "generated-index", - "description": "Begin your journey with the Enjin ecosystem here. This section provides essential introductory guides and foundational knowledge." - } -} - \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/01-creating-collections.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/01-creating-collections.md deleted file mode 100644 index b981831..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/01-creating-collections.md +++ /dev/null @@ -1,271 +0,0 @@ ---- -title: "Creating Collections" -slug: "creating-collections" -description: "Discover how to create and manage collections in the Enjin ecosystem, allowing you to organize blockchain assets efficiently." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info What you'll need: -- Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for and 6.25 ENJ for s. -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -::: - -On Enjin Blockchain, every token must be placed inside a . - -**There are two ways to use the functionalities:** - -1. [Using the Enjin Dashboard](#option-a-using-the-enjin-dashboard) -2. [Using the Enjin API & SDKs](#option-b-using-the-enjin-api--sdks) - -## Option A. Using the Enjin Dashboard - -In the Platform menu, navigate to "**[Collections](https://platform.canary.enjin.io/collections)**". Then, click the "**[Create Collection](https://platform.canary.enjin.io/create/collection)**" button. - -![Creating a collection](/img/guides/managing-tokens/create-collection.gif) - -From here, you can customize your collection's Mint Policy, Market Policy, Explicit Currencies (on option to delegate a token for ), and Attributes. - -- **[Mint Policy](/03-api-reference/04-important-arguments.md#mintpolicy) -** The rules pertaining to token supply and number of tokens available to be minted in the future. -- **[Market Policy](/03-api-reference/04-important-arguments.md#marketpolicy) -** Determines the rules which tokens in this collection must follow when interacting with the on-chain marketplace. -- **[Explicit Royalty Currencies](/03-api-reference/04-important-arguments.md#explicitroyaltycurrencies) -** Choose which currencies are required to pay marketplace royalties for the tokens in this collection. -- **[Attributes](/03-api-reference/04-important-arguments.md#attributes) -** Add details to your collection using key-value pairs. Standard keys like `name` and `description` allow applications to display your content correctly. You can also use the `URI` to link to a JSON file hosting your metadata. For more information, see the [Metadata Standard](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md) page. - -:::info Learn more about the arguments -For a comprehensive view and detail of all available arguments please refer to our [API Reference](/03-api-reference/03-api-reference.md). -::: - -Once you're satisfied with the options, click on the "**Create**" button at the bottom right corner to create the request. - -The Transaction Request will then appear in the "**Transactions**" menu. - -

- Collection created banner that appears after creating a transaction request -

- -![Pending create collection transaction](/img/guides/managing-tokens/pending-create-collection-txn.png) - -Since this request requires a , it'll need to be signed with your Wallet. - -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. - -Once you've created a collection you're ready to start [creating tokens](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md). - -## Option B. Using the Enjin API & SDKs - -The `CreateCollection` mutation is used to create a new on-chain collection. - - - -```graphql -mutation CreateCollection { - CreateCollection(mintPolicy: { forceCollapsingSupply: false }) #Set to true to enforce collapsing supply mint policy - { - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation CreateCollection($forceCollapsingSupply: Boolean) {\r\n CreateCollection(\r\n mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"forceCollapsingSupply":false}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the mutation -var createCollection = new CreateCollection() - .SetMintPolicy(new MintPolicy().SetForceCollapsingSupply(false)); //Set to true to enforce collapsing supply mint policy - -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -createCollection.Fragment(transactionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendCreateCollection(createCollection); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreMutations.hpp" -#include -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Setup mutation data - shared_ptr mintPolicy = make_shared(); - mintPolicy->SetForceSingleMint(make_shared(false)); - - // Setup mutation - CreateCollection createCollection = CreateCollection(); - createCollection.SetMintPolicy(mintPolicy); - - // Define and assign the return data fragment to the mutation - shared_ptr transactionFragment = make_shared(); - transactionFragment - ->WithId() - .WithMethod() - .WithState(); - - createCollection.SetFragment(transactionFragment); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>>> futureResponse = SendCreateCollection(*client, createCollection); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& transaction = gqlResult->GetData()->GetResult(); - - std::cout << to_string(transaction->GetId().value()) << std::endl; - std::cout << ToString(transaction->GetMethod().value()) << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation CreateCollection($forceCollapsingSupply: Boolean) { - CreateCollection( - mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } - ) { - id - method - state - } - } - `, - variables: { - forceCollapsingSupply: false //Set to true to enforce collapsing supply mint policy - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation CreateCollection($forceCollapsingSupply: Boolean) { - CreateCollection( - mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } - ) { - id - method - state - } - } - `, - variables: { - forceCollapsingSupply: false //Set to true to enforce collapsing supply mint policy - } -}, { - headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation CreateCollection($forceCollapsingSupply: Boolean) { - CreateCollection( - mintPolicy: { forceCollapsingSupply: $forceCollapsingSupply } - ) { - id - method - state - } -} -''' - -variables = {'forceCollapsingSupply': False} #Set to true to enforce collapsing supply mint policy - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -:::info Explore More Arguments -For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -For instance, you'll find settings such as different supply types with the `MintPolicy` argument, enforcing royalties with the `MarketPolicy` argument, or adding metadata with the `AttributeInput` argument. -::: - -A WebSocket event will also be fired so you can pick up the collection in real time by listening to the app channel on the WebSocket. - -:::tip What's next? -You've created a collection, now [Fill it with Tokens](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) -::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md deleted file mode 100644 index 71e9273..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/010-transfer-accept-collection.md +++ /dev/null @@ -1,343 +0,0 @@ ---- -title: "Transfer / Accept collection ownership" -slug: "transfer-accept-collection" -description: "Explore how to transfer and accept entire blockchain collections, allowing for seamless movement of grouped assets within the Enjin ecosystem." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info What you'll need: -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -- If you are transferring collection ownership, you also need to own a [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md). -::: - -Collection ownership on the Enjin Blockchain grants specific permissions and control over a collection and its tokens. -The collection owner is the only account authorized to make changes to the collection or any tokens within it. This includes the exclusive ability to mint new tokens or mint additional supply of existing tokens in the collection. - -Transferring collection ownership is done in two steps: - -## Step #1: Sending a transfer ownership request with the [Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) - -To send a transfer ownership request, we use the `MutateCollection` mutation: - - - -```graphql -mutation SendTransferOwnershipRequest { - MutateCollection( - collectionId: 36105 #Specify the collection ID - mutation: { - owner: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the new owner - } - ) { - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation SendTransferOwnershipRequest(\r\n $collection_id: BigInt!\r\n $new_owner: String\r\n) {\r\n MutateCollection(\r\n collectionId: $collection_id\r\n mutation: { owner: $new_owner }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"new_owner":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Set up the collection mutation input. -var collectionMutationInput = new CollectionMutationInput() - .SetOwner("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"); // Set the new owner account of the collection. - -// Set up the mutation -var mutateCollection = new MutateCollection() - .SetCollectionId(36105) // Specify the collection ID. - .SetMutation(collectionMutationInput); // Specify the mutation input. - -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -mutateCollection.Fragment(transactionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendMutateCollection(mutateCollection); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -Work in progress -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation SendTransferOwnershipRequest( - $collection_id: BigInt! - $new_owner: String - ) { - MutateCollection( - collectionId: $collection_id - mutation: { - owner: $new_owner - } - ) { - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - new_owner: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the new owner - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation SendTransferOwnershipRequest( - $collection_id: BigInt! - $new_owner: String - ) { - MutateCollection( - collectionId: $collection_id - mutation: { - owner: $new_owner - } - ) { - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - new_owner: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the new owner - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation SendTransferOwnershipRequest( - $collection_id: BigInt! - $new_owner: String -) { - MutateCollection( - collectionId: $collection_id - mutation: { - owner: $new_owner - } - ) { - id - method - state - } -} -''' - -variables = { - 'collection_id': 36105, #Specify the collection ID - 'new_owner': 'cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f' #Specify the new owner - -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -Once the transaction is confirmed, the new owner needs to approve the ownership transfer request, for the collection ownership to be executed - -## Step #2: Accepting a transfer ownership request with the [Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) - -To accept a transfer ownership request, we call the `AcceptCollectionTransfer` mutation from the new collection owner account: - - - -```graphql -mutation AcceptTransferOwnershipRequest { - AcceptCollectionTransfer( - collectionId: 36105 #Specify the collection ID - ) { - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation AcceptTransferOwnershipRequest($collection_id: BigInt!) {\r\n AcceptCollectionTransfer(collectionId: $collection_id) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Set up the mutation -var acceptCollectionTransfer = new AcceptCollectionTransfer() - .SetCollectionId(36105); // Specify the collection ID. - -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -acceptCollectionTransfer.Fragment(transactionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendAcceptCollectionTransfer(acceptCollectionTransfer); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -Work in progress -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation AcceptTransferOwnershipRequest( - $collection_id: BigInt! - ) { - AcceptCollectionTransfer( - collectionId: $collection_id - ) { - id - method - state - } - } - `, - variables: { - collection_id: 36105 //Specify the collection ID - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation AcceptTransferOwnershipRequest( - $collection_id: BigInt! - ) { - AcceptCollectionTransfer( - collectionId: $collection_id - ) { - id - method - state - } - } - `, - variables: { - collection_id: 36105 //Specify the collection ID - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation AcceptTransferOwnershipRequest( - $collection_id: BigInt! -) { - AcceptCollectionTransfer( - collectionId: $collection_id - ) { - id - method - state - } -} -''' - -variables = { - 'collection_id': 36105 #Specify the collection ID - -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -Once the transaction is confirmed, the collection ownership will be transferred to the new owner. diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md deleted file mode 100644 index c0c1bac..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-a-currency-token.md +++ /dev/null @@ -1,286 +0,0 @@ ---- -title: "Creating a Currency Token" -slug: "../creating-a-currency-token" ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Similarly to [Tokens](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md), "Currency Tokens" are also digital assets that can be traded, sold, or used on the Enjin Blockchain. However, unlike standard tokens, currency tokens support fractional values, allowing them to be divided into smaller units, just like traditional currencies. - -:::info What you'll need: -- Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to process transactions and at least 0.01 ENJ for the . -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) to place the tokens in. -::: - -Enjin Blockchain allows you to create customized structures. This flexibility enables you to organize your tokens in various ways that suit your needs. - -For the currency token options, configure the metadata parameter, which consists of 3 values: - -- **Name:** The token name (e.g. Gold Coins). -- **Symbol:** A short symbol that represents the token (e.g. GOLD). -- **Decimals:** Max amount of decimals supported for this token (e.g. 2 would mean the token can support up to 2 decimals, like 5.75 Gold Coins). - -:::warning Important Note on Decimals Support -The `decimalCount` property specifies how token balances should be displayed in applications, but it does not allow for actual fractional values on the Enjin Blockchain. -This means that for a token with "Decimals: 2", the balance should be divided by 100 (10^2) when displayed in applications. -For example, for the Gold Coins token example mentioned above, a balance of 1,575 Gold Coins should be shown as 15.75 (1,575/100) in apps. -Similarly, when minting tokens, to mint 4.80 Gold Coins, the minted supply parameter should be set to 480 (4.8 \* 10^2). -::: - -:::tip Token ID Structure [Best Practices](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md) -Before minting the Mainnet versions of your Tokens, that will be used in your live economy. Make sure to take a look at the [best practices for Token ID structure](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md). -::: - -**There are two ways to use the functionalities:** - -- [Using the Platform User Interface](#option-a-using-the-enjin-dashboard) -- [Using the GraphQL API](#option-b-using-the-enjin-api--sdks) - -## Option A. Using the Enjin Dashboard - -:::warning Unavailable via the User Interface -The option to create a currency token via the user interface is currently being developed. -For the time being, you can create currency token via [Option B. Using the Enjin API & SDKs](#option-b-using-the-enjin-api--sdks). -::: - -## Option B. Using the Enjin API & SDKs - -CreateToken mutation enables you to create a new token within an existing collection. This operation is essential for introducing new digital assets, and it allows you to define various attributes and characteristics for the newly created token. -For the currency token options, configure the metadata parameter, which consists of name, symbol and decimals - - - -```graphql -mutation CreateCurrencyToken{ - CreateToken( - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #The recipient of the initial supply - collectionId: 2406 #Set the collection ID - params:{ - tokenId: {integer: 0} #Set the token ID - initialSupply: 1 #Mint initial supply - cap: {type: INFINITE} #Define supply type - metadata: {name: "Gold Coins", symbol: "GOLD", decimalCount: 2} #Define currency configuration here - } - ) { - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation CreateCurrencyToken(\r\n $recipient: String!\r\n $collection_id: BigInt!\r\n $token_id: BigInt\r\n $initial_supply: BigInt\r\n $cap: TokenMintCapType!\r\n $metadata: TokenMetadataInput\r\n) {\r\n CreateToken(\r\n recipient: $recipient\r\n collectionId: $collection_id\r\n params: {\r\n tokenId: { integer: $token_id }\r\n initialSupply: $initial_supply\r\n cap: { type: $cap }\r\n metadata: $metadata\r\n }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","collection_id":2406,"token_id":0,"initial_supply":1,"cap":"INFINITE","metadata":{"name":"Gold Coins","symbol":"GOLD","decimalCount":2}}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Define the token metadata input -var tokenMetadata = new TokenMetadataInput() - .SetName("Token Name") // Set the token name - .SetSymbol("TKN") // Set the token symbol - .SetDecimalCount(18); // Set the decimal count - -// Define the token parameters -var tokenParams = new CreateTokenParams() - .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) //Set the token ID - .SetInitialSupply(1) //Mint initial supply - .SetCap(new TokenMintCap().SetType(TokenMintCapType.Infinite)) //Define supply type - .SetMetadata(tokenMetadata); //Set the token metadata - -// Set up the mutation -var createToken = new CreateToken() - .SetRecipient("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f") //The recipient of the initial supply - .SetCollectionId(2406) //Set the collection ID - .SetParams(tokenParams); //Set the previously defined token params - -// Define and assign the return data fragment to the mutation -var createTokenFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -createToken.Fragment(createTokenFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendCreateToken(createToken); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -Work in Progress! -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation CreateCurrencyToken - ( - $recipient: String! - $collection_id: BigInt! - $token_id: BigInt - $initial_supply: BigInt - $cap: TokenMintCapType! - $metadata: TokenMetadataInput - ) { - CreateToken( - recipient: $recipient - collectionId: $collection_id - params:{ - tokenId: {integer: $token_id} - initialSupply: $initial_supply - cap: {type: $cap} - metadata: $metadata - } - ) { - id - method - state - } - } - `, - variables: { - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //The recipient of the initial supply - collection_id: 2406, //Specify the collection ID - token_id: 0, //Specify the token ID - initial_supply: 1, //Mint initial supply - cap: "INFINITE", //Define supply type - metadata: {name: "Gold Coins", symbol: "GOLD", decimalCount: 2} //Define currency configuration here - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation CreateCurrencyToken( - $recipient: String! - $collection_id: BigInt! - $token_id: BigInt - $initial_supply: BigInt - $cap: TokenMintCapType! - $metadata: TokenMetadataInput - ) { - CreateToken( - recipient: $recipient - collectionId: $collection_id - params: { - tokenId: { integer: $token_id } - initialSupply: $initial_supply - cap: { type: $cap } - metadata: $metadata - } - ) { - id - method - state - } - } - `, - variables: { - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //The recipient of the initial supply - collection_id: 2406, //Specify the collection ID - token_id: 0, //Specify the token ID - initial_supply: 1, //Mint initial supply - cap: "INFINITE", //Define supply type - metadata: {name: "Gold Coins", symbol: "GOLD", decimalCount: 2} //Define currency configuration here - } -}, { - headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation CreateCurrencyToken( - $recipient: String! - $collection_id: BigInt! - $token_id: BigInt - $initial_supply: BigInt - $cap: TokenMintCapType! - $metadata: TokenMetadataInput -) { - CreateToken( - recipient: $recipient - collectionId: $collection_id - params: { - tokenId: { integer: $token_id } - initialSupply: $initial_supply - cap: { type: $cap } - metadata: $metadata - } - ) { - id - method - state - } -} -''' - -variables = { - 'recipient': 'cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f', #The recipient of the initial supply - 'collection_id': 2406, #Specify the collection ID - 'token_id': 0, #Specify the token ID - 'initial_supply': 1, #Mint initial supply - 'cap': 'INFINITE', #Define supply type - 'metadata': {'name': "Gold Coins", 'symbol': "GOLD", 'decimalCount': 2} #Define currency configuration here -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -A WebSocket event will also be fired so you can pick up the changes in real-time by listening to the app channel on the WebSocket. - -:::tip -For Token ID management, head to [Best Practices > TokenID Structure](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md) -::: - -:::info Explore More Arguments -For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -For instance, you'll find settings such as adding attributes/royalties/supply type and much more with the `CreateTokenParams` argument, or the ability to sign using a managed wallet with the `signingAccount` argument. -::: - -:::tip What's next? -To add metadata to your token, go to the [Adding Metadata](/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md) tutorial. -::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md deleted file mode 100644 index 82edea4..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md +++ /dev/null @@ -1,364 +0,0 @@ ---- -title: "Creating Tokens" -slug: "../creating-tokens" -description: "A step-by-step guide on how to create custom tokens using the Enjin Platform, enabling developers to mint blockchain assets with ease." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -"s" are digital assets that can be traded, sold, or used on the Enjin Blockchain. - -:::info What you'll need: -- Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to process transactions and at least 0.01 ENJ for the . -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) to place the tokens in. -::: - -Enjin Blockchain allows you to create customized structures. This flexibility enables you to organize your tokens in various ways that suit your needs. - -- **For s** These tokens are identical in value and function, and therefore, they all share the same Token ID. This commonality in ID reflects their interchangeable nature. For instance, all units of a specific cryptocurrency like Bitcoin would have the same identification as they hold the same value and are indistinguishable from one another in terms of usage and worth. -- **For (s):** Every NFT has a unique Token ID that sets it apart from other tokens, even within the same collection. This unique ID is crucial for establishing the individuality and provenance of each NFT, which could represent anything from digital art to ownership rights over a virtual asset. - -:::tip Token ID Structure [Best Practices](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md) -Before minting the Mainnet versions of your Tokens, that will be used in your live economy. Make sure to take a look at the [best practices for Token ID structure](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md). -::: - -**There are two ways to use the functionalities:** - -- [Using the Platform User Interface](#option-a-using-the-enjin-dashboard) -- [Using the GraphQL API](#option-b-using-the-enjin-api--sdks) - -## Option A. Using the Enjin Dashboard - -In the Platform menu, navigate to "**[Tokens](https://platform.enjin.io/tokens)**". Then, click the "**[Create Token](https://platform.enjin.io/create/token)**" button. - -![Creating Token](/img/guides/managing-tokens/creating-token.gif) - -From here, you can customize your collection's Mint Policy, Market Policy, and Attributes. - -- **Create Token Section -** Basic token options. Make sure to select the Collection ID you wish to mint the token in, the token ID, and the recipient in the corresponding fields. - Make sure to check out the [TokenID Structure Best Practices](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md). -- **[Cap](/03-api-reference/04-important-arguments.md#cap) -** The token cap (if required). - - The **[Infinite](/03-api-reference/04-important-arguments.md#infinite)** supply type is the most flexible. With this model, there is no limit to how many tokens can be minted or be in circulation. The collection owner can always mint additional units, making it ideal for use cases that require an ever-expanding token supply. - - The **[Fixed](/03-api-reference/04-important-arguments.md#supply)** supply type offers a balanced approach between flexibility and control. This model allows the collection owner to mint new tokens as long as the circulating supply does not exceed the predetermined max supply. Burned tokens can be re-minted, ensuring the total supply remains constant. - - The **[Collapsing](/03-api-reference/04-important-arguments.md#collapsing_supply)** supply type is the most strict. This supply type allows the collection owner to mint new tokens as long as the circulating supply does not exceed the max supply. However, burning tokens reduces the max supply, meaning burned tokens cannot be re-minted. This ensures a non-increasing supply, suitable for use cases that require strict control over the token's total amount in circulation. -- **[Token Royalty Settings](/03-api-reference/04-important-arguments.md#hasroyalty) -** Configures royalties for each marketplace sale of this token. -- **[Attributes](/03-api-reference/04-important-arguments.md#attributes) -** Add details to your token using key-value pairs. Standard keys like `name` and `description` allow applications to display your content correctly. You can also use the `URI` to link to a JSON file hosting your metadata. For more information, see the [Metadata Standard](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md) page. - -:::info Learn more about the arguments -For a comprehensive view and detail of all available arguments please refer to our [API Reference](/03-api-reference/03-api-reference.md). -::: - -Once you're satisfied with the options, click on the "**Create**" button at the bottom right corner to create the request. - -The Transaction Request will then appear in the "**Transactions**" menu. - -

- Create Token Transaction Request Banner -

- -![Pending Create Token Transaction](/img/guides/managing-tokens/pending-create-token-txn.png) - -Since this request requires a , it'll need to be signed with your Wallet. - -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. - -Once your token is created, lets give it a new look by [Adding Metadata](/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md) - -## Option B. Using the Enjin API & SDKs - -CreateToken mutation enables you to create a new token within an existing collection. This operation is essential for introducing new digital assets, and it allows you to define various attributes and characteristics for the newly created token. - - - -```graphql -mutation CreateToken{ - CreateToken( - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #The recipient of the initial supply - collectionId: 2406 #Set the collection ID - params:{ - tokenId: {integer: 0} #Set the token ID - initialSupply: 1 #Mint initial supply - cap: {type: INFINITE}} #Define supply type - ) { - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation CreateToken(\r\n $recipient: String!\r\n $collection_id: BigInt!\r\n $token_id: BigInt\r\n $initial_supply: BigInt\r\n $cap: TokenMintCapType!\r\n) {\r\n CreateToken(\r\n recipient: $recipient\r\n collectionId: $collection_id\r\n params: {\r\n tokenId: { integer: $token_id }\r\n initialSupply: $initial_supply\r\n cap: { type: $cap }\r\n }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","collection_id":91829,"token_id":0,"initial_supply":1,"cap":"INFINITE"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Define the token parameters -var tokenParams = new CreateTokenParams() - .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) //Set the token ID - .SetInitialSupply(1) //Mint initial supply - .SetCap(new TokenMintCap().SetType(TokenMintCapType.Infinite)); //Define supply type - -// Setup the mutation -var createToken = new CreateToken() - .SetRecipient("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f") //The recipient of the initial supply - .SetCollectionId(2406) //Set the collection ID - .SetParams(tokenParams); //Set the previously defined token params - -// Define and assign the return data fragment to the mutation -var createTokenFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -createToken.Fragment(createTokenFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendCreateToken(createToken); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreMutations.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Setup mutation data - shared_ptr tokenParams = make_shared(); - - shared_ptr tokenId = make_shared(); - tokenId->SetInteger(make_shared("0")); - - shared_ptr tokenMintCap = make_shared(); - tokenMintCap->SetType(TokenMintCapType::Infinite); - - tokenParams->SetTokenId(tokenId); - tokenParams->SetInitialSupply(make_shared("1")); - tokenParams->SetCap(tokenMintCap); - - // Setup mutation - CreateToken createToken = CreateToken(); - createToken - .SetRecipient(make_shared("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f")) - .SetCollectionId(make_shared("2406")) - .SetParams(tokenParams); - - // Define and assign the return data fragment to the mutation - shared_ptr transactionFragment = make_shared(); - transactionFragment - ->WithId() - .WithMethod() - .WithState(); - - createToken.SetFragment(transactionFragment); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>>> futureResponse = SendCreateToken(*client, createToken); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& transaction = gqlResult->GetData()->GetResult(); - - std::cout << to_string(transaction->GetId().value()) << std::endl; - std::cout << ToString(transaction->GetMethod().value()) << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation CreateToken( - $recipient: String! - $collection_id: BigInt! - $token_id: BigInt - $initial_supply: BigInt - $cap: TokenMintCapType! - ) { - CreateToken( - recipient: $recipient - collectionId: $collection_id - params: { - tokenId: { integer: $token_id } - initialSupply: $initial_supply - cap: { type: $cap } - } - ) { - id - method - state - } - } - `, - variables: { - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //The recipient of the initial supply - collection_id: 2406, //Specify the collection ID - token_id: 0, //Specify the token ID - initial_supply: 1, //Mint initial supply - cap: "INFINITE" //Define supply type - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation CreateToken( - $recipient: String! - $collection_id: BigInt! - $token_id: BigInt - $initial_supply: BigInt - $cap: TokenMintCapType! - ) { - CreateToken( - recipient: $recipient - collectionId: $collection_id - params: { - tokenId: { integer: $token_id } - initialSupply: $initial_supply - cap: { type: $cap } - } - ) { - id - method - state - } - } - `, - variables: { - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //The recipient of the initial supply - collection_id: 2406, //Specify the collection ID - token_id: 0, //Specify the token ID - initial_supply: 1, //Mint initial supply - cap: "INFINITE" //Define supply type - } -}, { - headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation CreateToken( - $recipient: String! - $collection_id: BigInt! - $token_id: BigInt - $initial_supply: BigInt - $cap: TokenMintCapType! -) { - CreateToken( - recipient: $recipient - collectionId: $collection_id - params: { - tokenId: { integer: $token_id } - initialSupply: $initial_supply - cap: { type: $cap } - } - ) { - id - method - state - } -} -''' - -variables = { - 'recipient': 'cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f', #The recipient of the initial supply - 'collection_id': 2406, #Specify the collection ID - 'token_id': 0, #Specify the token ID - 'initial_supply': 1, #Mint initial supply - 'cap': 'INFINITE' #Define supply type -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -A WebSocket event will also be fired so you can pick up the changes in real-time by listening to the app channel on the WebSocket. - -:::tip -For Token ID management, head to [Best Practices > TokenID Structure](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md) -::: - -:::info Explore More Arguments -For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -For instance, you'll find settings such as adding attributes/royalties/supply type and much more with the `CreateTokenParams` argument, or the ability to sign using a managed wallet with the `signingAccount` argument. -::: - -:::tip What's next? -To add metadata to your token, go to the [Adding Metadata](/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md) tutorial. -::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/_category_.json deleted file mode 100644 index 023c625..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/02-creating-tokens/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Creating Tokens" -} - \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md deleted file mode 100644 index 0d4a079..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md +++ /dev/null @@ -1,456 +0,0 @@ ---- -title: "Adding Metadata" -slug: "adding-metadata" -description: "Learn how to create currency tokens using Enjin's platform, perfect for powering blockchain economies within games and applications." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Adding to a token enables games and apps to import detailed information and showcase your token's data. Usually, digital assets are identified just by its . Metadata adds extra details to these assets, giving them properties such as a title, a narrative, and visuals. - -:::info What you'll need: -- Some [ Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for and metadata s. -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to add metadata to. -- Follow our [Best Practices for Collection/Token Metadata](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md). -::: - -You can even use metadata to attach gifs, videos, and 3D models to your token. This data can be stored both on-chain and off-chain to allow users and other supported platforms to access the information. - -When storing data on-chain, metadata can be defined as attributes such as name, description, fallback image, and any other attribute you choose. However, if the data is stored off-chain, it should always reference a centralized public repository through a .json file. - -## Metadata Storage - -There are three main ways to store metadata for your tokens: - -1. **On-Chain Storage: Attributes** - - Storing metadata directly on the blockchain is known as on-chain storage. - - It's immutable and highly secure since it's protected by the Enjin Blockchain's decentralized nature. - - On-chain storage can cost a small fee at the time of mint and has slight limitations in terms of space, making it suitable for smaller, permanent files. - -2. **Off-Chain Storage: JSON** - - This means the metadata is stored outside the blockchain on a separate server or database. - - It's often accessible through a URL, typically pointing to a JSON file containing the asset's information. - - Off-chain storage is cheaper and more flexible for large or frequently updated data, but it relies on external servers' availability and security. - -3. **InterPlanetary File System (): JSON** - - IPFS is a decentralized storage solution that distributes file storage across a network of computers. - - It allows metadata to be stored off the main blockchain but in a way that's still decentralized, tamper-proof, and permanent. - - IPFS assigns a unique hash to each file. When you store metadata on IPFS, you link to it using this hash, ensuring the data remains unchanged. - -Each option has trade-offs between cost, reliability, and security, and the choice depends on the specific needs and goals of the digital asset being created. - -## Attributes: OnChain Metadata - -You can assign multiple on-chain attributes to a token simultaneously, which allows you to define the metadata of the token. - -Some commonly recognized attributes, following the [Universal Metadata Standard](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md), include: - -- **`name`:** The title of the token. -- **`description`:** A brief explanation or description of the token. -- **`media`:** A token image, GIF, or MP4 to represent the token. -- **`URI`:** The web address (URL) where the JSON file containing the token's off-chain or IPFS metadata is hosted. - -:::tip Need to add more metadata? -Check the [Best Practices for Collection/Token Metadata](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md), and the [Universal Metadata Standard](https://enj.in/metadata). -::: - -:::warning Attributes for Collections and Tokens are very similar. -The process of adding attributes is similar for both collections and tokens. -While this tutorial guides you through adding an attribute to a token, you can follow the same steps to add attributes to a collection. -Simply navigate to the corresponding menu for collections instead of tokens. -::: - -:::warning **Important:** Attribute keys are case sensitive. -Ensure you use the correct casing when defining attributes to avoid errors. -::: - -**There are two ways to add metadata:** - -1. [Using the Enjin Dashboard](#option-a-using-the-enjin-dashboard) -2. [Using the GraphQL API & SDKs](#option-b-using-the-enjin-api--sdks) - -### Option A. Using the Enjin Dashboard - -In the Platform menu, navigate to "**[Tokens](https://platform.canary.enjin.io/tokens)**". -**Locate the token** you wish to add / edit attributes for, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Attributes**" button. - -![Adding Metadata](/img/guides/managing-tokens/adding-metadata.gif) - -:::tip Need to add multiple attributes for a token? -Click on the "**Batch**" button, followed by "**Batch SetAttribute**". -::: - -To add / edit an attribute, select the "**Set**" option, type in the "**Key**" that you wish to add / edit, and it's "**Value**" in the corresponding text fields. - -Once you're satisfied with the options, click on the "**Set Attribute**" button at the bottom right corner to create the request. - -

- Set Attribute form -

- -The Transaction Request will then appear in the "**Transactions**" menu. - -

- Set Attribute Banner -

- -![Pending Set Attribute Banner](/img/guides/managing-tokens/pending-set-attr-txn.png) - -Since this request requires a , it'll need to be signed with your Wallet. - -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. - -### Option B. Using the Enjin API & SDKs - -The BatchSetAttribute mutation allows you to efficiently set or update multiple attributes for a specific token within a collection in a single blockchain transaction. Attributes represent various properties, characteristics, or metadata associated with a token. - - - -```graphql -mutation BatchSetAttribute { - BatchSetAttribute( - collectionId: 36105 # Specify the collection ID - tokenId: {integer: 0 } #Specify the token ID. If you wish to add collection metadata, omit this line entirely. - attributes: [ - { - key: "name" #Provide an attribute name - value: "Chronicles of the Celestium" #Provide an attribute value - }, - { - key: "description" #Provide an attribute name - value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." #Provide an attribute value - }, - { - key: "uri" #Provide an attribute name - value: "https://yourhost/metadata.json" #Provide an attribute value - } - ] - ) { - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation BatchSetAttribute($collection_id: BigInt!, $token_id: BigInt) {\r\n BatchSetAttribute(\r\n collectionId: $collection_id\r\n tokenId: { integer: $token_id }\r\n attributes: [\r\n { key: \"name\", value: \"Chronicles of the Celestium\" }\r\n {\r\n key: \"description\"\r\n value: \"An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse.\"\r\n }\r\n { key: \"uri\", value: \"https://yourhost/metadata.json\" }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":0}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -//Define attributes to set -var attributes = new List -{ - new AttributeInput() - .SetKey("name") //Provide an attribute name - .SetValue("Chronicles of the Celestium"), //Provide an attribute value - new AttributeInput() - .SetKey("description") //Provide an attribute name - .SetValue("An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse."), //Provide an attribute value - new AttributeInput() - .SetKey("uri") //Provide an attribute name - .SetValue("https://yourhost/metadata.json") //Provide an attribute value -}; - -// Setup the mutation -var batchSetAttribute = new BatchSetAttribute() - .SetCollectionId(36105) //Specify the collection ID - .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) //Specify the token ID. If you wish to add collection metadata, omit this line entirely. - .SetAttributes(attributes.ToArray()); //Set the previously defined attributes as an array - -// Define and assign the return data fragment to the mutation -var batchSetAttributeFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -batchSetAttribute.Fragment(batchSetAttributeFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendBatchSetAttribute(batchSetAttribute); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreMutations.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - //Define attributes to set - vector vectorAttributes; - AttributeInput nameAttribute1 = AttributeInput() - .SetKey(make_shared("name")) - .SetValue(make_shared("Chronicles of the Celestium")); - - AttributeInput nameAttribute2 = AttributeInput() - .SetKey(make_shared("description")) - .SetValue(make_shared("An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse.")); - - AttributeInput nameAttribute3 = AttributeInput() - .SetKey(make_shared("uri")) - .SetValue(make_shared("https://yourhost/metadata.json")); - - vectorAttributes.push_back(nameAttribute1); - vectorAttributes.push_back(nameAttribute2); - vectorAttributes.push_back(nameAttribute3); - - shared_ptr> attributes = make_shared>(vectorAttributes); - - // Setup mutation data - shared_ptr tokenId = make_shared(); - tokenId->SetInteger(make_shared("0")); - - // Setup mutation - BatchSetAttribute batchSetAttribute = BatchSetAttribute(); - batchSetAttribute - .SetCollectionId(make_shared("2406")) - .SetTokenId(tokenId) - .SetAttributes(attributes); - - // Define and assign the return data fragment to the mutation - shared_ptr transactionFragment = make_shared(); - transactionFragment - ->WithId() - .WithMethod() - .WithState(); - - batchSetAttribute.SetFragment(transactionFragment); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>>> futureResponse = SendBatchSetAttribute(*client, batchSetAttribute); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& transaction = gqlResult->GetData()->GetResult(); - - std::cout << to_string(transaction->GetId().value()) << std::endl; - std::cout << ToString(transaction->GetMethod().value()) << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation BatchSetAttribute($collection_id: BigInt!, $token_id: BigInt) { - BatchSetAttribute( - collectionId: $collection_id - tokenId: { integer: $token_id } - attributes: [ - { - key: "name" - value: "Chronicles of the Celestium" - } - { - key: "description" - value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." - } - { - key: "uri" - value: "https://yourhost/metadata.json" - } - ] - ) { - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0 //Specify the token ID. - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation BatchSetAttribute($collection_id: BigInt!, $token_id: BigInt) { - BatchSetAttribute( - collectionId: $collection_id - tokenId: { integer: $token_id } - attributes: [ - { key: "name", value: "Chronicles of the Celestium" } - { - key: "description" - value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." - } - { key: "uri", value: "https://yourhost/metadata.json" } - ] - ) { - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0 //Specify the token ID. - } -}, { - headers: { 'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here' } -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation BatchSetAttribute($collection_id: BigInt!, $token_id: BigInt) { - BatchSetAttribute( - collectionId: $collection_id - tokenId: { integer: $token_id } - attributes: [ - { key: "name", value: "Chronicles of the Celestium" } - { - key: "description" - value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." - } - { key: "uri", value: "https://yourhost/metadata.json" } - ] - ) { - id - method - state - } -} -''' - -variables = { - 'collection_id': 36105, #Specify the collection ID - 'token_id': 0, #Specify the token ID. -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -A WebSocket event will also be fired so you can pick up the changes in real-time by listening to the app channel on the WebSocket. - -## JSON File: Off-Chain & IPFS Metadata - -A JSON file can be used to provide comprehensive information about the asset, including its description, visual representation, multimedia attachments, external references, categorization, associated files, and specific attributes. - -Take a look at this simple JSON schema (following the [Universal Metadata Standard](https://enj.in/metadata)) to learn how to properly format the off-chain metadata for your NFT. - -```json -{ - "name": "Starforged Cleaver", - "description": "A blade smithed from cosmic steel, its edge sharp enough to slice through the fabric of reality itself.", - "external_url": "https://enjin.io", - "fallback_image": "https://yourhost/image.jpg", - "media": [ - { - "type": "image/png", - "url": "https://yourhost/image.jpg" - } - ], - "attributes": { - "Strength": { - "value": "800" - } - } -} -``` - -Take note of these data points: - -- **`description`**: This field provides a human-readable description of the asset. It's a text description that helps users understand the nature or significance of the asset. - -- **`media`**: The `media` field contains a URL pointing to an image representation of the asset. It supports PNG, GIF, and JPG file formats. - -- **`external_url`**: The "external_url" field is a URL pointing to an external application or website where users can view additional information or interact with the asset. - -- **`attributes`**: This is an object array where each object should contain an `title` and `value` fields. The attribute `title` defines the type of trait or attribute associated with the asset, while `value` can be a string or a number, representing the value of that trait or attribute. - -You can also host your JSON files on IPFS, to make your metadata more immutable. - -:::tip Need to add more metadata? -Check the [Best Practices for Collection/Token Metadata](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md), and the [Universal Metadata Standard](https://enj.in/metadata). -::: - -:::info Explore More Arguments -For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. - -For instance, you'll find settings such as `continueOnFailure` to skip data that would cause the whole batch to fail, or the ability to sign using a managed wallet with the `signingAccount` argument. -::: - -:::tip What's next? -To mint some token supply, head to the [Minting Tokens](/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md) tutorial. -::: \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md deleted file mode 100644 index 9f33619..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/04-minting-a-token.md +++ /dev/null @@ -1,339 +0,0 @@ ---- -title: "Minting Tokens" -slug: "minting-a-token" -description: "Enhance your blockchain assets by adding custom metadata, providing additional details and functionalities to your tokens." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Now that you've got your tokens created, it's time to start minting them into player wallets as they are earned. - -We call this "Play-to-Mint," giving players the power to create tokens themselves, which feels more rewarding. - -Plus, it keeps your processes more efficient by delivering tokens right to players' wallets without any unnecessary `transfer` transactions. - -:::info What you'll need: -- Some [ Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for and a deposit of 0.01 ENJ is required for the , for each new token holder. -If the token has , each new unit minted will require the same amount of ENJ to be infused. -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to mint. -::: - -**There are two ways to use the functionalities:** - -1. [Using the Enjin Dashboard](#option-a-using-the-enjin-dashboard) -2. [Using the GraphQL API & SDKs](#option-b-using-the-enjin-api--sdks) - -## Option A. Using the Enjin Dashboard - -In the Platform menu, navigate to "**[Tokens](https://platform.canary.enjin.io/tokens)**". -**Locate the token** you wish to mint, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Mint**" button. - -![Minting a Token](/img/guides/managing-tokens/minting-a-token.gif) - -Set the recipient and the amount in the corresponding fields, and Click on "**Mint**" - -

- Mint Token Form -

- -The Transaction Request will then appear in the "**Transactions**" menu. - -

- Mint Transaction Request Banner -

- -![Pending Mint Transaction](/img/guides/managing-tokens/pending-mint-txn.png) - -Since this request requires a , it'll need to be signed with your Wallet. - -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. - -## Option B. Using the Enjin API & SDKs - -The BatchMint mutation enables you to efficiently create multiple tokens within a single blockchain transaction. This process, known as batch minting, simplifies the minting of multiple tokens, reducing transaction fees and processing time. - - - -```graphql -mutation BatchMint { - BatchMint( - collectionId: "7154" #Specify the collection ID - recipients: [ - { - account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" #The recipient of the mint - mintParams: { - amount:1 #Amount to mint - tokenId: {integer: 6533} #Token ID to mint - } - } - ] - ) { - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation BatchMint($collection_id: BigInt!) {\r\n BatchMint(\r\n collectionId: $collection_id\r\n recipients: [\r\n {\r\n account: \"0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921\"\r\n mintParams: {\r\n amount: 1\r\n tokenId: { integer: 6533 }\r\n }\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"collection_id":7154}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Define the list of recipients and their mint parameters -var recipients = new List -{ - new MintRecipient() - .SetAccount("0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921") - .SetMintParams(new MintTokenParams() - .SetAmount(1) - .SetTokenId(new EncodableTokenIdInput().SetInteger(6533)) - ) -}; - -// Setup the mutation -var batchMint = new BatchMint() - .SetCollectionId(7154) - .SetRecipients(recipients.ToArray()); - -// Define and assign the return data fragment to the mutation -var batchMintFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -batchMint.Fragment(batchMintFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendBatchMint(batchMint); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreMutations.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Define the list of recipients and their mint parameters - shared_ptr tokenId = make_shared(); - tokenId->SetInteger(make_shared("0")); - - MintTokenParams mintTokenParams = MintTokenParams() - .SetAmount(make_shared("1")) - .SetTokenId(tokenId); - - - MintRecipient mintRecipient = MintRecipient() - .SetAccount(make_shared("0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921")) - .SetMintParams(make_shared(mintTokenParams)); - - vector recipients; - recipients.push_back(mintRecipient); - - // Setup mutation - BatchMint batchMint = BatchMint() - .SetCollectionId(make_shared("7154")) - .SetRecipients(make_shared>(recipients)); - - // Define and assign the return data fragment to the mutation - shared_ptr transactionFragment = make_shared(); - transactionFragment - ->WithId() - .WithMethod() - .WithState(); - - batchMint.SetFragment(transactionFragment); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>>> futureResponse = SendBatchMint(*client, batchMint); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& transaction = gqlResult->GetData()->GetResult(); - - std::cout << to_string(transaction->GetId().value()) << std::endl; - std::cout << ToString(transaction->GetMethod().value()) << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation BatchMint( - $collection_id: BigInt! - ) { - BatchMint( - collectionId: $collection_id - recipients: [ - { - account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" #The recipient of the mint - mintParams: { - amount:1 #Amount to mint - tokenId: {integer: 6533} #Token ID to mint - } - } - ] - ) { - id - method - state - } - } - `, - variables: { - collection_id: 7154 //Specify the collection ID - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation BatchMint( - $collection_id: BigInt! - ) { - BatchMint( - collectionId: $collection_id - recipients: [ - { - account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" #The recipient of the mint - mintParams: { - amount:1 #Amount to mint - tokenId: {integer: 6533} #Token ID to mint - } - } - ] - ) { - id - method - state - } - } - `, - variables: { - collection_id: 7154 //Specify the collection ID - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation BatchMint( - $collection_id: BigInt! -) { - BatchMint( - collectionId: $collection_id - recipients: [ - { - account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" #The recipient of the mint - mintParams: { - amount:1 #Amount to mint - tokenId: {integer: 6533} #Token ID to mint - } - } - ] - ) { - id - method - state - } -} -''' - -variables = { - 'collection_id': 7154 #Specify the collection ID -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -A WebSocket event will also be fired so you can pick up the transfer transaction in real time by listening to the app channel on the WebSocket. - -:::info Explore More Arguments -For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. - -For instance, you'll find settings such as the ability to sign using a managed wallet with the `signingAccount` argument, or batch create tokens instead of minting existing ones with the `createParams` argument. -::: - -:::tip You've minted a token! -What if you need to transfer a token? proceed to [Transferring Tokens](/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md). -::: \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md deleted file mode 100644 index e249128..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/05-transferring-tokens.md +++ /dev/null @@ -1,724 +0,0 @@ ---- -title: "Transferring Tokens" -slug: "transferring-tokens" -description: "Find out how to securely transfer tokens between users or platforms using the Enjin Platform, ensuring safe and efficient asset movement." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -You will need to transfer tokens for: - -- Implementing various gameplay features such as gifting, trading, and rewards. -- Supporting secure and efficient token transactions. -- Providing a seamless user experience by allowing token transfers without leaving the game environment. - -:::info What you'll need: -- Some [ Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for and a deposit of 0.01 ENJ is required for the , for each new token holder. -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to mint. -::: - -**There are two ways to transfer a token:** - -1. [Using the Enjin Dashboard](#option-a-using-the-enjin-dashboard) -2. [Using the GraphQL API & SDKs](#option-b-using-the-enjin-api--sdks) - -## Option A. Using the Enjin Dashboard - -In the Platform menu, navigate to "**[Tokens](https://platform.canary.enjin.io/tokens)**". -**Locate the token** you wish to transfer, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Transfer**" button. - -![Transferring Token](/img/guides/managing-tokens/transferring-token.gif) - -:::info Need to perform multiple transfers? -Click on the "**Batch**" button, followed by "**Batch Transfer**". -::: - -Fill in the recipient, amount, and other optional arguments in the corresponding fields. -Once you're satisfied with the options, click on the "**Transfer**" button at the bottom right corner to create the request. - -

- Transfer Token Form -

- -

- Transfer Token Transaction Banner -

- -![Pending Transfer Transaction](/img/guides/managing-tokens/pending-transfer-txn.png) -Clicking "**View**" on the notification will take you to your Transactions List. - -Since this request requires a , it'll need to be signed with your Wallet. - -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. - -If you're looking to distribute tokens to your community or players, but don't have their account addresses, don't worry! Our solution is Enjin Beam. -Proceed to the [Distributing Tokens via QR](/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md) tutorial to learn more. - -## Option B. Using the Enjin API & SDKs - -### Transferring an asset - -`SimpleTransferToken` mutation simplifies the process of transferring a specific token from one wallet to another. It is a straightforward way to facilitate token transfers without the need for complex intermediary steps. - - - -```graphql -mutation TransferToken{ - SimpleTransferToken( - collectionId: 36105 #Specify the collection ID - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the recipent address - params: { - tokenId: {integer: 0} #Specify the token ID - amount: 1 #Choose the transfer amount - } - ){ - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation TransferToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $recipient: String!\r\n $amount: BigInt!\r\n) {\r\n SimpleTransferToken(\r\n collectionId: $collection_id\r\n recipient: $recipient\r\n params: { tokenId: { integer: $token_id }, amount: $amount }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"collection_id":36105,"token_id":0,"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","amount":1}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Define the simple transfer parameters -var simpleTransferParams = new SimpleTransferParams() - .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) - .SetAmount(1); - - -// Setup the mutation -var simpleTransferToken = new SimpleTransferToken() - .SetRecipient("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f") - .SetCollectionId(36105) - .SetParams(simpleTransferParams); - -// Define and assign the return data fragment to the mutation -var simpleTransferTokenFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -simpleTransferToken.Fragment(simpleTransferTokenFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendSimpleTransferToken(simpleTransferToken); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreMutations.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Define the simple transfer params - shared_ptr tokenId = make_shared(); - tokenId->SetInteger(make_shared("0")); - - SimpleTransferParams simpleTransferParams = SimpleTransferParams() - .SetAmount(make_shared("1")) - .SetTokenId(tokenId); - - // Setup mutation - SimpleTransferToken simpleTransferToken = SimpleTransferToken() - .SetRecipient(make_shared("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f")) - .SetCollectionId(make_shared("36105")) - .SetParams(make_shared(simpleTransferParams)); - - // Define and assign the return data fragment to the mutation - shared_ptr transactionFragment = make_shared(); - transactionFragment - ->WithId() - .WithMethod() - .WithState(); - - simpleTransferToken.SetFragment(transactionFragment); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>>> futureResponse = SendSimpleTransferToken(*client, simpleTransferToken); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& transaction = gqlResult->GetData()->GetResult(); - - std::cout << to_string(transaction->GetId().value()) << std::endl; - std::cout << ToString(transaction->GetMethod().value()) << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} - -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation TransferToken( - $collection_id: BigInt! - $token_id: BigInt! - $recipient: String! - $amount: BigInt! - ) { - SimpleTransferToken( - collectionId: $collection_id - recipient: $recipient - params: { - tokenId: {integer: $token_id} - amount: $amount - } - ){ - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0, //Specify the token ID - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address - amount: 1 //Choose the transfer amount - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation TransferToken( - $collection_id: BigInt! - $token_id: BigInt! - $recipient: String! - $amount: BigInt! - ) { - SimpleTransferToken( - collectionId: $collection_id - recipient: $recipient - params: { - tokenId: {integer: $token_id} - amount: $amount - } - ){ - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0, //Specify the token ID - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address - amount: 1 //Choose the transfer amount - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation TransferToken( - $collection_id: BigInt! - $token_id: BigInt! - $recipient: String! - $amount: BigInt! -) { - SimpleTransferToken( - collectionId: $collection_id - recipient: $recipient - params: { - tokenId: {integer: $token_id} - amount: $amount - } - ){ - id - method - state - } -} -''' - -variables = { - 'collection_id': 36105, #Specify the collection ID - 'token_id': 0, #Specify the token ID - 'recipient': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", #Specify the recipent address - 'amount': 1 #Choose the transfer amount -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -### Transferring ENJ token - -To send ENJ / CENJ tokens from one wallet to another, use the `TransferAllowDeath` mutation, or the `TransferKeepAlive` if you want to make sure the account doesn't get reaped ([Read more about account reaping](https://support.enjin.io/hc/en-gb/articles/16297132519569-What-is-the-Existential-Deposit)): - - - -```graphql -mutation TransferENJTokens { - TransferKeepAlive( - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the recipent address - amount: 5000000000000000000 #Specify the amount of tokens to transfer - ) { - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation TransferENJTokens(\r\n $recipient: String!\r\n $amount: BigInt!\r\n $keep_alive: Boolean\r\n) {\r\n TransferBalance(\r\n recipient: $recipient\r\n amount: $amount\r\n keepAlive: $keep_alive\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"recipient":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","amount":1,"keep_alive":true}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Set up the mutation -var transferKeepAlive = new TransferKeepAlive() - .SetRecipient("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f") //The recipient of the initial supply - .SetAmount(5000000000000000000); //The amount of tokens to transfer - -// Define and assign the return data fragment to the mutation -var transferKeepAliveFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -transferKeepAlive.Fragment(transferKeepAliveFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendTransferKeepAlive(transferKeepAlive); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -Work In Progress -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation TransferKeepAlive( - $recipient: String! - $amount: BigInt! - ) { - TransferBalance( - recipient: $recipient - amount: $amount - ){ - id - method - state - } - } - `, - variables: { - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address - amount: 1 //Specify the amount of tokens to transfer - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation TransferKeepAlive( - $recipient: String! - $amount: BigInt! - ) { - TransferBalance( - recipient: $recipient - amount: $amount - ){ - id - method - state - } - } - `, - variables: { - recipient: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address - amount: 1 //Specify the amount of tokens to transfer - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation TransferKeepAlive( - $recipient: String! - $amount: BigInt! -) { - TransferBalance( - recipient: $recipient - amount: $amount - ){ - id - method - state - } -} -''' - -variables = { - 'recipient': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", #Specify the recipent address - 'amount': 1 #Specify the amount of tokens to transfer - -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -:::info **Key Arguments** -- **Calculating the `amount`** - - The Platform accepts ENJ values in the **base unit** (integers), not decimal ENJ amounts. To calculate the correct input, **multiply your desired ENJ amount by 10^18** (1 quintillion). - - **Formula:** `Desired ENJ` \* `1,000,000,000,000,000,000` - - **Example:** To transfer **5 ENJ**, input `5000000000000000000`. -- **Using `keepAlive`** - - Set to `true` to prevent the account from being removed (reaped) if the balance drops below the minimum requirement. - - [Learn more about the keepAlive argument here](/03-api-reference/04-important-arguments.md#keepalive) -::: - -### Batch Transferring ENJ token - -To send ENJ / CENJ tokens from one wallet to multiple addresses or in multiple transactions, we use the `BatchTransferBalance` mutation - - - -```graphql -mutation BatchSendENJ{ - BatchTransferBalance( - recipients: [ - { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", #Specify the recipent address - transferBalanceParams: { - value: 5000000000000000000 #Specify the amount of tokens to transfer - } - }, - { - account: "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", #Specify the recipent address - transferBalanceParams: { - value: 15250000000000000000 #Specify the amount of tokens to transfer - keepAlive: true #Set to true if you want to make sure the account doesn't get reaped - } - }, - ] - ){ - id - transactionId - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation BatchSendENJ($recipients: [TransferRecipient!]!) {\r\n BatchTransferBalance(recipients: $recipients) {\r\n id\r\n transactionId\r\n state\r\n }\r\n}\r\n","variables":{"recipients":[{"account":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f","transferBalanceParams":{"value":5000000000000000000}},{"account":"cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu","transferBalanceParams":{"value":15250000000000000000,"keepAlive":true}}]}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Set up the transfers for the batch -var transferBalanceParams1 = new TransferBalanceParams() - .SetValue(5000000000000000000); //The amount of tokens to transfer - -var transferBalanceParams2 = new TransferBalanceParams() - .SetValue(5000000000000000000) //The amount of tokens to transfer - .SetKeepAlive(true); //Whether the transaction will be kept from failing if the balance drops below the minimum requirement - -var transferRecipients = new[] -{ - new TransferRecipient() - .SetAccount("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f") - .SetTransferBalanceParams(transferBalanceParams1), - new TransferRecipient() - .SetAccount("cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu") - .SetTransferBalanceParams(transferBalanceParams2) -}; - -// Set up the mutation -var batchTransferBalance = new BatchTransferBalance() - .SetRecipients(transferRecipients); - -// Define and assign the return data fragment to the mutation -var transferBalanceFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -batchTransferBalance.Fragment(transferBalanceFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendBatchTransferBalance(batchTransferBalance); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -Work In Progress -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation BatchSendENJ($recipients: [TransferRecipient!]!){ - BatchTransferBalance( - recipients: $recipients - ){ - id - transactionId - state - } - } - `, - variables: { - recipients:[ - { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address - transferBalanceParams: { value: 5000000000000000000 } //Specify the amount of tokens to transfer - }, - { - account: "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", //Specify the recipent address - transferBalanceParams: { - value: 15250000000000000000, //Specify the amount of tokens to transfer - keepAlive: true //Set to true if you want to make sure the account doesn't get reaped - } - } - ] - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation BatchSendENJ($recipients: [TransferRecipient!]!){ - BatchTransferBalance( - recipients: $recipients - ){ - id - transactionId - state - } - } - `, - variables: { - recipients:[ - { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", //Specify the recipent address - transferBalanceParams: { value: 5000000000000000000 } //Specify the amount of tokens to transfer - }, - { - account: "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", //Specify the recipent address - transferBalanceParams: { - value: 15250000000000000000, //Specify the amount of tokens to transfer - keepAlive: true //Set to true if you want to make sure the account doesn't get reaped - } - } - ] - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation BatchSendENJ($recipients: [TransferRecipient!]!){ - BatchTransferBalance( - recipients: $recipients - ){ - id - transactionId - state - } -} -''' - -variables = { - 'recipients':[ - { - 'account': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f", #Specify the recipent address - 'transferBalanceParams': { 'value': 5000000000000000000 } #Specify the amount of tokens to transfer - }, - { - 'account': "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", #Specify the recipent address - 'transferBalanceParams': { - 'value': 15250000000000000000, #Specify the amount of tokens to transfer - 'keepAlive': True #Set to true if you want to make sure the account doesn't get reaped - } - } - ] -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -A WebSocket event will also be fired so you can pick up the transfer transaction in real time by listening to the app channel on the WebSocket. - -:::tip Need to send a transaction request to user's wallet? -This can be done using Enjin Platform API & WalletConnect! -To learn more, check out the [Using WalletConnect page](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md). -::: - -:::info Explore More Arguments -For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -For instance, you'll find settings such as `continueOnFailure` to skip data that would cause the whole batch to fail, or the ability to sign using a managed wallet with the `signingAccount` argument. -::: - -:::What's next? -Distribute tokens to your players, even if they don't have wallets! -Proceed to the [Distributing Tokens via QR](/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md) tutorial to learn more. -::: \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md deleted file mode 100644 index 306a541..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md +++ /dev/null @@ -1,995 +0,0 @@ ---- -title: "Create QR Drops" -slug: "create-qr-drops" -description: "Discover how to create QR drops, allowing for simple distribution of blockchain tokens to users via scannable QR codes." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - - is a simple way to distribute digital assets like tokens and collectibles using QR codes. Users just need to scan a Beam QR code with their Enjin Wallet to receive tokens directly. It's a great tool for marketing campaigns, events, or promotions to give away items and easily bring new users into your ecosystem. - -:::info What you'll need: -- Some [ Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for and a deposit of 0.01 ENJ is required for the , for each new token holder. -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to mint. -::: - -**There are two ways to create an Enjin Beam:** - -1. [Using the Enjin Dashboard](#option-a-using-the-enjin-dashboard) -2. [Using the Enjin API & SDKs](#option-b-using-the-enjin-api--sdks) - -## Option A. Using the Enjin Dashboard - -1. Log in to your Enjin Platform account. -2. In the main menu, go to "**Beams**" ([https://platform.canary.enjin.io/beams/list](https://platform.canary.enjin.io/beams/list)). -3. Click the "**Create Enjin Beam**" button ([https://platform.canary.enjin.io/create/beam](https://platform.canary.enjin.io/create/beam)). - -![Creating a Beam](/img/guides/managing-tokens/create-beam.gif) - -Now, you'll fill out the details for your Beam: - -- **Beam Settings:** Configure the basic properties of your Beam. - - **Collection ID:** Select the collection that contains the tokens you want to distribute. - - **Beam Metadata:** Give your Beam a name, description, and add an image. This is what users will see when they scan the QR code. - -

- Create Beam Form -

- -- **Tokens to Distribute:** This is where you define which tokens are given out and how many. - - A single Beam is made up of one or more "Beam Claims". Each time a user scans the QR code and successfully receives a token, they are redeeming one **Beam Claim**. - - **[Token IDs](/03-api-reference/04-important-arguments.md#tokenid):** Specify the IDs of the specific tokens you want to distribute with this claim. - - **[Claim Quantity](/03-api-reference/04-important-arguments.md#claimquantity):** This sets how many individual Beam Claims you want to create for each selected Token ID. - - **[Token Quantity Per Claim](/03-api-reference/04-important-arguments.md#tokenquantityperclaim):** This sets how many units of the token a user receives *each time* they redeem a single Beam Claim. For example, if you're giving out stacks of 5 gold coins with each claim, set this to 5. - - **[Type](/03-api-reference/04-important-arguments.md#beam-claim-type):** Choose whether redeeming a claim will `MINT` a brand new token or `TRANSFER` an existing token from your account balance. - -

- Create Beam Form 2 -

- -:::tip Use **[Flags](/03-api-reference/04-important-arguments.md#flags)** for advanced options and more control - - **Single-use:** This important flag changes how the QR code works. - - **Default (Single-use OFF):** You get one main QR code for the entire Beam. When a user scans it, the system randomly picks one of the available Beam Claims to give them. - - **Single-use ON:** A *unique QR code* is generated for *every single Beam Claim*. - - **Why use Single-use?** It gives you precise control over which specific claim (and thus which token) goes to which person. It's also more secure for private distributions; you can share individual QR codes with specific users without worrying about a main QR code being leaked publicly. -::: - -Once you're satisfied with the options, click on the "**Create**" button at the bottom right corner to create the request. -If a signature request is needed, approve it. - -Once the Beam is created, locate it in the "**Beams**" menu, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Claim**" button to view the QR code. - -![Claiming a Beam](/img/guides/managing-tokens/claim-beam.gif) - -:::warning Need to get all of the "Single-Use" QR codes? -Please note, viewing all of the "Single-Use" QR codes is currently not available in the Platform User Interface. -For the time being, it can be viewed programmatically only via [Enjin API & SDKs](#option-b-using-the-enjin-api--sdks) -::: - -The Enjin Beam is now ready to be shared to distribute tokens. Users can scan the QR code and instantly receive your token! -Next skillset to learn is mutating a collection / token. -Proceed to the [Freezing & Thawing](/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md) tutorial to learn more. - -## Option B. Using the Enjin API & SDKs - -:::info Make sure to use the Beam Endpoint -- Testnet: `http://platform.canary.enjin.io/graphql/beam` -- Mainnet: `http://platform.enjin.io/graphql/beam` - -**Try the Beam Playground here:** -- Testnet: http://platform.canary.enjin.io/graphiql/beam -- Mainnet: http://platform.enjin.io/graphiql/beam -::: - -First, create the Beam with the parameters of your choice, such as name, description, image, begin and end date & the number of assets that you would like to distribute. - -**Mutation:** - - - -```graphql -mutation CreateBeam { - CreateBeam( - name: "My Beam" #Specify beam name - description: "This is a test beam." #Specify beam description - image: "https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg" #Specify url for beam image - shows during claim process - start: "1st January 2023" #Specify start date - end: "1st January 2024" #Specify end date - collectionId: 36623 #Select the collection ID - tokens: [{ - tokenIds: ["1..10", "123"] #Select the Token IDs. Use comma for multiple tokenIds: ["123", "234"], 2 dots for range: ["123", "234..345"] - tokenQuantityPerClaim:1 #Specify the number of tokens a user can receive per claim - claimQuantity:1 #Specify the number of times a user can claim - type:MINT_ON_DEMAND #Select token distribution type - }] - ) -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation CreateBeam(\r\n $name: String!\r\n $description: String!\r\n $image: String!\r\n $start: DateTime!\r\n $end: DateTime!\r\n $collection_id: BigInt!\r\n) {\r\n CreateBeam(\r\n name: $name\r\n description: $description\r\n image: $image\r\n start: $start\r\n end: $end\r\n collectionId: $collection_id\r\n tokens: [\r\n {\r\n tokenIds: [\"1..10\", \"123\"]\r\n tokenQuantityPerClaim: 1\r\n claimQuantity: 1\r\n type: MINT_ON_DEMAND\r\n }\r\n ]\r\n )\r\n}\r\n","variables":{"name":"My Beam","description":"This is a test beam.","image":"https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg","start":"1st January 2023","end":"1st January 2024","collection_id":36623}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; -using Enjin.Platform.Sdk.Beam; - -Cl -var tokens = new List -{ - new ClaimToken() - .SetTokenIds(new List - { - new IntegerRange(1, 10), - new IntegerRange(123) - }.ToArray() - ) - .SetClaimQuantity(1) - .SetTokenQuantityPerClaim(1) - .SetType(BeamType.MintOnDemand) -}; - -// Setup the mutation -var createBeam = new CreateBeam() - .SetName("My Beam") - .SetDescription("This is a test beam.") - .SetImage("https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg") - .SetStart(new DateTime(2023, 1, 1)) - .SetEnd(new DateTime(2024, 1, 1)) - .SetCollectionId(36623) - .SetTokens(tokens.ToArray()); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendCreateBeam(createBeam); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdkBeam/BeamMutations.hpp" -#include "EnjinPlatformSdkBeam/Beam.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace enjin::platform::sdk::beam; -using namespace std; - -int main() { - - // Define the list of tokens and claim parameters to include in the Beam - vector claimTokens; - vector integerRanges; - integerRanges.emplace_back("1", "10"); - integerRanges.emplace_back("123"); - - ClaimToken claimToken1 = ClaimToken() - .SetTokenIds(make_shared>(integerRanges)) - .SetClaimQuantity(make_shared(1)) - .SetTokenQuantityPerClaim(make_shared(1)) - .SetType(BeamType::MintOnDemand); - - claimTokens.push_back(claimToken1); - - DateTime startAt = DateTime::Parse("2023-01-01T00:00:00+0"); // Note the date string should be in ISO 8601 format. - DateTime endAt = DateTime::Parse("2024-01-01T00:00:00+0"); // Note the date string should be in ISO 8601 format. - - // Setup mutation - CreateBeam createBeam = CreateBeam() - .SetName(make_shared("My Beam")) - .SetDescription(make_shared("This is a test beam.")) - .SetImage(make_shared("https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg")) - .SetStart(make_shared(startAt)) - .SetEnd(make_shared(endAt)) - .SetCollectionId(make_shared("36623")) - .SetTokens(make_shared>(claimTokens)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendCreateBeam(*client, createBeam); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& beamCode = gqlResult->GetData()->GetResult(); - - std::cout << beamCode.value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation CreateBeam - ( - $name: String! - $description: String! - $image: String! - $start: DateTime! - $end: DateTime! - $collection_id: BigInt! - ) { - CreateBeam( - name: $name - description: $description - image: $image - start: $start - end: $end - collectionId: $collection_id - tokens: [{ - tokenIds: ["1..10", "123"] #Select the Token IDs. Use comma for multiple tokenIds ["123", "234"], 2 dots for range: ["123", "234..345"] - tokenQuantityPerClaim:1 #Specify the number of tokens a user can receive per claim - claimQuantity:1 #Specify the number of times a user can claim - type:MINT_ON_DEMAND #Select token distribution type - }] - ) - } - `, - variables: { - name: "My Beam", //Specify beam name - description: "This is a test beam.", //Specify beam description - image: "https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg", //Specify url for beam image - shows during claim process - start: "1st January 2023", //Specify start date - end: "1st January 2024", //Specify end date - collection_id: 36623 //Select the collection ID - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation CreateBeam - ( - $name: String! - $description: String! - $image: String! - $start: DateTime! - $end: DateTime! - $collection_id: BigInt! - ) { - CreateBeam( - name: $name - description: $description - image: $image - start: $start - end: $end - collectionId: $collection_id - tokens: [{ - tokenIds: ["1..10" ,"123"] #Select the Token IDs. Use comma for multiple tokenIds: ["123", "234"], 2 dots for range: ["123", "234..345"] - tokenQuantityPerClaim:1 #Specify the number of tokens a user can receive per claim - claimQuantity:1 #Specify the number of times a user can claim - type:MINT_ON_DEMAND #Select token distribution type - }] - ) - } - `, - variables: { - name: "My Beam", //Specify beam name - description: "This is a test beam.", //Specify beam description - image: "https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg", //Specify url for beam image - shows during claim process - start: "1st January 2023", //Specify start date - end: "1st January 2024", //Specify end date - collection_id: 36623 //Select the collection ID - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation CreateBeam -( - $name: String! - $description: String! - $image: String! - $start: DateTime! - $end: DateTime! - $collection_id: BigInt! -) { - CreateBeam( - name: $name - description: $description - image: $image - start: $start - end: $end - collectionId: $collection_id - tokens: [{ - tokenIds: ["1..10", "123"] #Select the Token IDs. Use comma for multiple tokenIds: ["123", "234"], 2 dots for range: ["123", "234..345"] - tokenQuantityPerClaim:1 #Specify the number of tokens a user can receive per claim - claimQuantity:1 #Specify the number of times a user can claim - type:MINT_ON_DEMAND #Select token distribution type - }] - ) -} -''' - -variables = { - 'name': "My Beam", #Specify beam name - 'description': "This is a test beam.", #Specify beam description - 'image': "https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg", #Specify url for beam image - shows during claim process - 'start': "1st January 2023", #Specify start date - 'end': "1st January 2024", #Specify end date - 'collection_id': 36623 #Select the collection ID -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -Next you will need to send a query to get one of the two different code types. - -- **Multi-Use Codes:** Useful for advertising. -- **Single-Use Codes:** Useful for rewarding individuals. - -**Query: Multi-Use Codes** - - - -```graphql -query GetBeam { - GetBeam(code: "adab7ac8bda71c2e2bf8a3228f0a435f") { #Set the beam code - id - code - name - description - image - start - end - isClaimable - claimsRemaining - qr { - url - payload - } - collection { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - } - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query GetBeam($code: String!) {\r\n GetBeam(code: $code) {\r\n id\r\n code\r\n name\r\n description\r\n image\r\n start\r\n end\r\n isClaimable\r\n claimsRemaining\r\n qr {\r\n url\r\n payload\r\n }\r\n collection {\r\n collectionId\r\n maxTokenCount\r\n maxTokenSupply\r\n forceSingleMint\r\n frozen\r\n network\r\n }\r\n }\r\n}\r\n","variables":{"code":"adab7ac8bda71c2e2bf8a3228f0a435f"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; -using Enjin.Platform.Sdk.Beam; - -// Setup the query -var getBeam = new GetBeam() - .SetCode("adab7ac8bda71c2e2bf8a3228f0a435f"); - -// Define and assign the return data fragment to the query -var beamFragment = new BeamFragment() - .WithId() - .WithCode()Wi - .WithName() - .WithDescription() - .WithImage() - .WithStart() - .WithEnd() - .WithQr(new BeamQrFragment() - .WithUrl() - .WithPayload() - ) - .WithCollection(new CollectionFragment() - .WithCollectionId() - .WithMaxTokenCount() - .WithMaxTokenSupply() - .WithForceSingleMint() - .WithFrozen() - .WithNetwork() - ); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendGetBeam(getBeam); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdkBeam/BeamQueries.hpp" -#include "EnjinPlatformSdkBeam/Beam.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace enjin::platform::sdk::beam; -using namespace std; - -int main() { - - // Set up the query - GetBeam getBeam = GetBeam() - .SetCode(make_shared("adab7ac8bda71c2e2bf8a3228f0a435f")); - - // Define the fragment for the returned data - BeamFragment beamFragment = BeamFragment() - .WithId() - .WithCode() - .WithName() - .WithDescription() - .WithImage() - .WithStart() - .WithEnd() - .WithQr(make_shared( - BeamQrFragment() - .WithUrl() - .WithPayload() - ) - ) - .WithCollection(make_shared( - CollectionFragment() - .WithCollectionId() - .WithMaxTokenCount() - .WithMaxTokenSupply() - .WithForceSingleMint() - .WithFrozen() - .WithNetwork() - ) - ); - - getBeam.SetFragment(make_shared(beamFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendGetBeam(*client, getBeam); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& beam = gqlResult->GetData()->GetResult(); - - std::cout << beam->GetCode().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query GetBeam - ( - $code: String! - ) { - GetBeam(code: $code) { - id - code - name - description - image - start - end - isClaimable - claimsRemaining - qr { - url - payload - } - collection { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - } - } - } - `, - variables: { - code: "adab7ac8bda71c2e2bf8a3228f0a435f" //Set the beam code - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - query GetBeam - ( - $code: String! - ) { - GetBeam(code: $code) { - id - code - name - description - image - start - end - isClaimable - claimsRemaining - qr { - url - payload - } - collection { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - } - } - } - `, - variables: { - code: "adab7ac8bda71c2e2bf8a3228f0a435f" //Set the beam code - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -query GetBeam -( - $code: String! -) { - GetBeam(code: $code) { - id - code - name - description - image - start - end - isClaimable - claimsRemaining - qr { - url - payload - } - collection { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - } - } -} -''' - -variables = { - 'code': "adab7ac8bda71c2e2bf8a3228f0a435f" #Set the beam code -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -**Query: Single-Use Codes** - - - -```graphql -query GetSingleUseCodes { - GetSingleUseCodes(code: "adab7ac8bda71c2e2bf8a3228f0a435f") { #Set the beam code - edges { - cursor - node { - qr { - url - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query GetSingleUseCodes($code: String!) {\r\n GetSingleUseCodes(code: $code) {\r\n edges {\r\n cursor\r\n node {\r\n qr {\r\n url\r\n }\r\n }\r\n }\r\n totalCount\r\n pageInfo {\r\n hasPreviousPage\r\n hasNextPage\r\n startCursor\r\n endCursor\r\n }\r\n }\r\n}\r\n","variables":{"code":"adab7ac8bda71c2e2bf8a3228f0a435f"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; -using Enjin.Platform.Sdk.Beam; - -// Setup the query -var getSingleUseCodes = new GetSingleUseCodes() - .SetCode("adab7ac8bda71c2e2bf8a3228f0a435f"); - -// Define and assign the return data fragment to the query -var beamConnectionFragment = new ConnectionFragment() - .WithEdges(new EdgeFragment() - .WithCursor() - .WithNode(new BeamClaimFragment() - .WithQr( - new BeamQrFragment() - .WithUrl() - ) - ) - ) - .WithTotalCount() - .WithPageInfo(new PageInfoFragment() - .WithHasPreviousPage() - .WithHasNextPage() - .WithStartCursor() - .WithEndCursor() - ); - -getSingleUseCodes.Fragment(beamConnectionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendGetSingleUseCodes(getSingleUseCodes); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdkBeam/BeamQueries.hpp" -#include "EnjinPlatformSdkBeam/Beam.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace enjin::platform::sdk::beam; -using namespace std; - -int main() { - - // Set up the query - GetSingleUseCodes getSingleUseCodes = GetSingleUseCodes() - .SetCode(make_shared("adab7ac8bda71c2e2bf8a3228f0a435f")); - - // Define Fragments for the returned data - BeamClaimFragment beamClaimFragment = BeamClaimFragment(); - BeamQrFragment beamQrFragment = BeamQrFragment().WithUrl(); - beamClaimFragment.WithQr(make_shared(beamQrFragment)); - - PageInfoFragment pageInfoFragment = PageInfoFragment() - .WithHasPreviousPage() - .WithHasNextPage() - .WithHasStartCursor() - .WithHasEndCursor(); - - // Connection fragments are used for paginated data - ConnectionFragment beamConnectionFragment = ConnectionFragment() - .WithEdges( - make_shared>( - EdgeFragment() - .WithCursor() - .WithNode(make_shared(beamClaimFragment)) - ) - ) - .WithTotalCount() - .WithPageInfo(make_shared(pageInfoFragment)); - - - getSingleUseCodes.SetFragment(make_shared>(beamConnectionFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>>> futureResponse = SendGetSingleUseCodes(*client, getSingleUseCodes); - - // Get the platform response holding the HTTP data - PlatformResponsePtr>> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional>& singleUseCodes = gqlResult->GetData()->GetResult(); - - std::cout << singleUseCodes->GetTotalCount().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} - -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query GetSingleUseCodes - ( - $code: String! - ) { - GetSingleUseCodes(code: $code) { - edges { - cursor - node { - qr { - url - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } - } - `, - variables: { - code: "adab7ac8bda71c2e2bf8a3228f0a435f" //Set the beam code - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - query GetSingleUseCodes - ( - $code: String! - ) { - GetSingleUseCodes(code: $code) { - edges { - cursor - node { - qr { - url - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } - } - `, - variables: { - code: "adab7ac8bda71c2e2bf8a3228f0a435f" //Set the beam code - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -query GetSingleUseCodes -( - $code: String! -) { - GetSingleUseCodes(code: $code) { - edges { - cursor - node { - qr { - url - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } -} -''' - -variables = { - 'code': "adab7ac8bda71c2e2bf8a3228f0a435f" #Set the beam code -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -:::tip This query might return items in different pages using Connections -To learn how to use GraphQL cursors for pagination, head to [Using the API --> Pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination). -::: - -The Enjin Beam is now ready to be shared to distribute tokens. Users can scan the QR code and instantly receive your token! - -:::info Explore More Arguments -For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. - -For instance, you'll find settings such as different Beam flags types with the `Flags` argument. -::: - -:::tip What's next? -Proceed to the [Freezing & Thawing](/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md) tutorial to learn more. -::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md deleted file mode 100644 index 677b54e..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/07-freezing-thawing.md +++ /dev/null @@ -1,1042 +0,0 @@ ---- -title: "Freezing & Thawing" -slug: "freezing-thawing" -description: "Learn how to freeze and thaw tokens, controlling when your blockchain assets can be transferred or used, for added flexibility." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -"Freezing" refers to the process of temporarily suspending the transferability of a collection or a specific token. This action provides you greater control over the movement of assets, enhancing security and enabling unique use-cases. - -One such use-case is the implementation of "Soulbound" tokens. A Soulbound token is bound to a specific address and cannot be transferred out of the wallet it's minted on. This feature can be used to create unique gameplay mechanics, loyalty rewards, and more. - -:::note -Freezing only applies to ing and transfers, which also results in marketplace listings being unpurchasable. -Freezing does not suspend token minting. -::: - -### Freeze States - -A **freeze state** determines whether a token can be transferred and the conditions under which it remains locked or becomes transferrable. This feature enables token creators to define unique behaviors and restrictions for their assets, supporting scenarios like enhanced security, gameplay mechanics, or compliance needs. - -#### Explanation of Freeze States - -- **Permanent:** - The token is permanently frozen and cannot be transferred to another account under any circumstances. Use this state for tokens that are intended to stay bound to their original holder, such as "Soulbound" tokens for identity or loyalty purposes. - -- **Temporary:** - The token is temporarily frozen, restricting transfers until it is explicitly thawed by the collection owner. This state is ideal for implementing time-limited restrictions or conditional asset movement. - -- **Never:** - The token is always transferrable and cannot be frozen. Choose this state if you want the token to remain unrestricted in its movement across wallets and platforms. - -:::info What you'll need: -- Some [ Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for . -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to freeze. -::: - -**There are two ways to Freeze / Thaw:** - -1. [Using the Enjin Dashboard](#option-a-using-the-enjin-dashboard) -2. [Using the GraphQL API & SDKs](#option-b-using-the-enjin-api--sdks) - -## Option A. Using the Enjin Dashboard - -:::tip Applying Freeze/Thaw Actions to Collections and Tokens -This tutorial illustrates the process of freezing a collection. -However, the same steps can be applied to freeze or thaw tokens. -Simply navigate to the corresponding menu for tokens instead of collections, or for thawing instead of freezing. -::: - -### Freezing an entire collection - -In the Platform menu, navigate to "**[Collections](https://platform.canary.enjin.io/collections)**". -**Locate the collection** you wish to freeze, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Freeze**" button. - -![Freezing a Collection](/img/guides/managing-tokens/freezing-collection.gif) - -Choose the freeze state, and click on the "**Freeze**" button. - -

- Freeze Collection Form -

- -The Transaction Request will then appear in the "**Transactions**" menu. - -

- Freeze Collection Transaction Request Banner -

- -![Pending Freeze Collection Transaction Request](/img/guides/managing-tokens/pending-freeze-collection-txn.png) - -Since this request requires a , it'll need to be signed with your Wallet. - -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. - -## Option B. Using the Enjin API & SDKs - -### Freezing a Collection or Token - -By freezing a collection, all tokens within that collection will be frozen, meaning they cannot be burned or transferred out of the wallet they're currently in. - -### Freezing an entire collection - -Use the `Freeze` mutation and the `freezeType: COLLECTION` argument to freeze a collection. - - - -```graphql -mutation FreezeCollection{ - Freeze( - collectionId: 36105 #Specify the collection ID - freezeType: COLLECTION #For collection freezing - ){ - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation FreezeCollection($collection_id: BigInt!, $freeze_type: FreezeType!) {\r\n Freeze(collectionId: $collection_id, freezeType: $freeze_type) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"freeze_type":"COLLECTION"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the mutation -var freezeCollection = new Freeze() - .SetCollectionId(36105) - .SetFreezeType(FreezeType.Collection); - -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -freezeCollection.Fragment(transactionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendFreeze(freezeCollection); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreMutations.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Set up the mutation - Freeze freezeCollection = Freeze() - .SetCollectionId(make_shared("36105")) - .SetFreezeType(FreezeType::Collection); - - // Define Fragment for the returned data - TransactionFragment transactionFragment = TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - - freezeCollection.SetFragment(make_shared(transactionFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendFreeze(*client, freezeCollection); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& freezeCollectionResult = gqlResult->GetData()->GetResult(); - - std::cout << freezeCollectionResult->GetId().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation FreezeCollection - ( - $collection_id: BigInt! - $freeze_type: FreezeType! - ) { - Freeze( - collectionId: $collection_id - freezeType: $freeze_type - ){ - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - freeze_type: "COLLECTION" //For collection freezing - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation FreezeCollection - ( - $collection_id: BigInt! - $freeze_type: FreezeType! - ) { - Freeze( - collectionId: $collection_id - freezeType: $freeze_type - ){ - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - freeze_type: "COLLECTION" //For collection freezing - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation FreezeCollection -( - $collection_id: BigInt! - $freeze_type: FreezeType! -) { - Freeze( - collectionId: $collection_id - freezeType: $freeze_type - ){ - id - method - state - } -} -''' - -variables = { - 'collection_id': 36105, #Specify the collection ID - 'freeze_type': "COLLECTION" #For collection freezing -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -Once the transaction is executed, all tokens within the specified collection will be frozen. - -### Freezing a single token - -Use the `Freeze` mutation and the `freezeType: TOKEN` argument to freeze a token. - - - -```graphql -mutation FreezeToken{ - Freeze( - collectionId: 36105 #Specify the collection ID - tokenId:{integer: 0} #Specify the token ID - freezeType: TOKEN #For token freezing - freezeState: TEMPORARY #Select the freeze state - ){ - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation FreezeToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $freeze_type: FreezeType!\r\n $freeze_state: FreezeStateType\r\n) {\r\n Freeze(\r\n collectionId: $collection_id\r\n tokenId: { integer: $token_id }\r\n freezeType: $freeze_type\r\n freezeState: $freeze_state\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":0,"freeze_type":"TOKEN","freeze_state":"TEMPORARY"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the mutation -var freezeToken = new Freeze() - .SetCollectionId(36105) - .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) - .SetFreezeType(FreezeType.Token) - .SetFreezeState(FreezeState.Temporary); - -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -freezeToken.Fragment(transactionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendFreeze(freezeToken); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreMutations.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Setup an Encodable Token ID - EncodableTokenIdInput tokenId = EncodableTokenIdInput() - .SetInteger(make_shared("0")); - - // Set up the mutation - Freeze freezeToken = Freeze() - .SetCollectionId(make_shared("36105")) - .SetTokenId(make_shared(tokenId)) - .SetFreezeType(FreezeType::Token) - .SetFreezeState(FreezeState::Temporary); - - // Define Fragment for the returned data - TransactionFragment transactionFragment = TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - - freezeToken.SetFragment(make_shared(transactionFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendFreeze(*client, freezeToken); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& freezeTokenResult = gqlResult->GetData()->GetResult(); - - std::cout << freezeTokenResult->GetId().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation FreezeToken - ( - $collection_id: BigInt! - $token_id: BigInt! - $freeze_type: FreezeType! - $freeze_state: FreezeStateType - - ) { - Freeze( - collectionId: $collection_id - tokenId:{integer: $token_id} - freezeType: $freeze_type - freezeState: $freeze_state - ){ - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0, //Specify the token ID - freeze_type: "TOKEN", //For token freezing - freeze_state: "TEMPORARY" //Select the freeze state - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation FreezeToken - ( - $collection_id: BigInt! - $token_id: BigInt! - $freeze_type: FreezeType! - $freeze_state: FreezeStateType - - ) { - Freeze( - collectionId: $collection_id - tokenId:{integer: $token_id} - freezeType: $freeze_type - freezeState: $freeze_state - ){ - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0, //Specify the token ID - freeze_type: "TOKEN", //For token freezing - freeze_state: "TEMPORARY" //Select the freeze state - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation FreezeToken -( - $collection_id: BigInt! - $token_id: BigInt! - $freeze_type: FreezeType! - $freeze_state: FreezeStateType - -) { - Freeze( - collectionId: $collection_id - tokenId:{integer: $token_id} - freezeType: $freeze_type - freezeState: $freeze_state - ){ - id - method - state - } -} -''' - -variables = { - 'collection_id': 36105, #Specify the collection ID - 'token_id': 0, #Specify the token ID - 'freeze_type': "TOKEN", #For token freezing - 'freeze_state': "TEMPORARY" #Select the freeze state -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -Once the transaction is executed, the specified token will be frozen. - -### Thawing a Collection or Token - -Thawing a collection means allowing all tokens within that collection to be active again, so they can be transferred or burned as desired, removing the restrictions that kept them locked in a particular wallet. - -### Thawing an entire collection - -By thawing a collection, all tokens within that collection will be thawed, meaning they can be burned and transferred out of the wallet they're currently in. - -Use the `Thaw` mutation and the `freezeType: COLLECTION` argument to thaw a collection. - - - -```graphql -mutation ThawCollection{ - Thaw( - collectionId: 36105 #Specify the collection ID - freezeType: COLLECTION #For collection thawing - ){ - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation ThawCollection($collection_id: BigInt!, $freeze_type: FreezeType!) {\r\n Thaw(collectionId: $collection_id, freezeType: $freeze_type) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"freeze_type":"COLLECTION"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the mutation -var thawCollection = new Thaw() - .SetCollectionId(36105) - .SetFreezeType(FreezeType.Collection); - -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -thawCollection.Fragment(transactionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendThaw(thawCollection); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreMutations.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Set up the mutation - Thaw thawCollection = Thaw() - .SetCollectionId(make_shared("36105")) - .SetFreezeType(FreezeType::Collection); - - // Define Fragment for the returned data - TransactionFragment transactionFragment = TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - - thawCollection.SetFragment(make_shared(transactionFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendThaw(*client, thawCollection); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& thawCollectionResult = gqlResult->GetData()->GetResult(); - - std::cout << thawCollectionResult->GetId().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation ThawCollection - ( - $collection_id: BigInt! - $freeze_type: FreezeType! - ) { - Thaw( - collectionId: $collection_id - freezeType: $freeze_type - ){ - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - freeze_type: "COLLECTION" //For collection thawing - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation ThawCollection - ( - $collection_id: BigInt! - $freeze_type: FreezeType! - ) { - Thaw( - collectionId: $collection_id - freezeType: $freeze_type - ){ - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - freeze_type: "COLLECTION" //For collection thawing - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation ThawCollection -( - $collection_id: BigInt! - $freeze_type: FreezeType! -) { - Thaw( - collectionId: $collection_id - freezeType: $freeze_type - ){ - id - method - state - } -} -''' - -variables = { - 'collection_id': 36105, #Specify the collection ID - 'freeze_type': "COLLECTION" #For collection thawing -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -Once the transaction is executed, all tokens within the specified collection will be thawed. - -### Thawing a single token - -Use the `Thaw` mutation and the `freezeType: TOKEN` argument to thaw a token. - - - -```graphql -mutation ThawToken{ - Thaw( - collectionId: 36105 #Specify the collection ID - tokenId:{integer: 0} #Specify the token ID - freezeType: TOKEN #For token thawing - ) { - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation ThawToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $freeze_type: FreezeType!\r\n) {\r\n Thaw(\r\n collectionId: $collection_id\r\n tokenId: { integer: $token_id }\r\n freezeType: $freeze_type\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":0,"freeze_type":"TOKEN"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the mutation -var thawToken = new Thaw() - .SetCollectionId(36105) - .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) - .SetFreezeType(FreezeType.Token); - -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -thawToken.Fragment(transactionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendThaw(thawToken); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreMutations.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Setup an Encodable Token ID - EncodableTokenIdInput tokenId = EncodableTokenIdInput() - .SetInteger(make_shared("0")); - - // Set up the mutation - Thaw thawToken = Thaw() - .SetCollectionId(make_shared("36105")) - .SetTokenId(make_shared(tokenId)) - .SetFreezeType(FreezeType::Token); - - // Define Fragment for the returned data - TransactionFragment transactionFragment = TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - - thawToken.SetFragment(make_shared(transactionFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendThaw(*client, thawToken); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& thawTokenResult = gqlResult->GetData()->GetResult(); - - std::cout << thawTokenResult->GetId().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} - -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation ThawToken - ( - $collection_id: BigInt! - $token_id: BigInt! - $freeze_type: FreezeType! - ) { - Thaw( - collectionId: $collection_id - tokenId:{integer: $token_id} - freezeType: $freeze_type - ) { - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0, //Specify the token ID - freeze_type: "TOKEN" //For token thawing - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation ThawToken - ( - $collection_id: BigInt! - $token_id: BigInt! - $freeze_type: FreezeType! - ) { - Thaw( - collectionId: $collection_id - tokenId:{integer: $token_id} - freezeType: $freeze_type - ) { - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0, //Specify the token ID - freeze_type: "TOKEN" //For token thawing - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation ThawToken -( - $collection_id: BigInt! - $token_id: BigInt! - $freeze_type: FreezeType! -) { - Thaw( - collectionId: $collection_id - tokenId:{integer: $token_id} - freezeType: $freeze_type - ) { - id - method - state - } -} -''' - -variables = { - 'collection_id': 36105, #Specify the collection ID - 'token_id': 0, #Specify the token ID - 'freeze_type': "TOKEN" #For token thawing -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -Once the transaction is executed, the specified token will be thawed. - -:::info Explore More Arguments -For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. - -For instance, you'll find settings such as `continueOnFailure` to skip data that would cause the whole batch to fail, or the ability to sign using a managed wallet with the `signingAccount` argument. -::: - -:::tip Need to send a transaction request to user's wallet? -This can be done using Enjin Platform API & WalletConnect! -To learn more, check out the [Using WalletConnect page](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md). -::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md deleted file mode 100644 index 55cea11..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/08-burning-destroying-tokens.md +++ /dev/null @@ -1,640 +0,0 @@ ---- -title: "Melting / Destroying Tokens and Collections" -slug: "burning-destroying-tokens" -description: "Understand the process of burning or destroying tokens in the Enjin ecosystem, allowing you to permanently remove assets from circulation." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -"Melting" (often called "Burning") refers to the process of decreasing a token's supply and removing it from circulation, or in some cases, even removing the token from the blockchain entirely. -Melting a token with releases the Infused ENJ to the holder. - -:::info What you'll need: -- Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for . -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) or a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to melt/destroy. -::: - -**There are two ways to Melt a token:** - -1. [Using the Platform User Interface](#option-a-using-the-enjin-dashboard) -2. [Using the GraphQL API](#option-b-using-the-enjin-api--sdks) - -## Option A. Using the Enjin Dashboard - -### Melting token's supply - -In the Platform menu, navigate to "**[Tokens](https://platform.canary.enjin.io/tokens)**". -**Locate the token** you wish to melt, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Burn**" button. - -![melting a Token](/img/guides/managing-tokens/burning-token.gif) - -Insert the amount of tokens to melt, and click on the "**Burn**" button. - -

- Melt Token Form -

- -The Transaction Request will then appear in the "**Transactions**" menu. - -

- Melt Transaction Request Banner -

- -![Pending melt Transaction](/img/guides/managing-tokens/pending-burn-token-txn.png) - -Since this request requires a , it'll need to be signed with your Wallet. - -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. - -### Destroying a token and removing it from the Blockchain - -:::info To destroy a token, these requirements must be met: -- The caller is the collection owner -- The token has no attributes - - If the token has attributes, you can remove the attributes by clicking the **3 vertical dots** (**⋮**) next to the token, followed by "**Attributes**" and selecting "**Remove All"**. -- The token has 0 supply - - If the token has supply, you can follow the above guide [Burning token's supply](#melting-tokens-supply) to remove all token supply (as long as you own all of the token's supply) - Note - you can remove the supply and destroy the token in the same melt transaction. -::: - -Melting a token and destroying it are two different actions. -The action demonstrated above is the action of melting a token, which decreases it's circulating supply. -While destroying a token removes the token from the blockchain, and retrieves the to the collection owner. - -To destroy a token, follow the above instructions for Melting a token, but make sure to tick the `Remove Token Storage` box. - -

- Destroying a Token -

- -### Destroying a collection - -:::info To destroy a collection, these requirements must be met: -- The caller is the collection owner -- The collection has no attributes - - If the collection has attributes, you can remove the attributes by clicking the **3 vertical dots** (**⋮**) next to the collection, followed by "**Attributes**" and selecting "**Remove All"**. -- The collection has 0 tokens **in storage** - - If the collection has some tokens, you can follow the above guide [Destroying a token and removing it from the Blockchain](#destroying-a-token-and-removing-it-from-the-blockchain) for each of the tokens in the collection, to destroy them all. -::: - -In the Platform menu, navigate to "**[Collections](https://platform.canary.enjin.io/collections)**". -**Locate the collection** you wish to destroy, click the **3 vertical dots** (**⋮**) to it's right, then click the "**Destroy**" button. - -![Destroying a Collection](/img/guides/managing-tokens/destroying-collection.gif) - -Then, confirm by clicking the "**Destroy**" button - -

- Destroy Collection Form -

- -The Transaction Request will then appear in the "**Transactions**" menu - -

- Destroy Collection Transaction Request Banner -

- -![Pending Destroy Collection Transaction](/img/guides/managing-tokens/pending-destroy-collection-txn.png) - -Since this request requires a , it'll need to be signed with your Wallet. - -- If a **Wallet Daemon is running and configured**, the transaction request will be **signed automatically**. -- If **a wallet is connected** such as the Enjin Wallet or Polkadot.js, the transaction must be **signed manually** by clicking the "**Sign**" button and **approving the signature request** in your wallet. - -## Option B. Using the Enjin API & SDKs - -### Melting token's supply - -Use the `Burn` mutation: - - - -```graphql -mutation BurnToken{ - Burn( - collectionId: 68844 #Specify the Collection ID - params: { - tokenId: {integer: 0} #Specify the Token ID - amount: 1 #Specify the amount of supply to burn - } - ) { - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation BurnToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $amount: BigInt!\r\n) {\r\n Burn(\r\n collectionId: $collection_id\r\n params: { tokenId: { integer: $token_id }, amount: $amount }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":5,"amount":1}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Set up the burn params -var burnParams = new BurnParamsInput() - .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) // Set the token id. - .SetAmount(1); // Set the amount to burn. - -// Set up the mutation -var burn = new Burn() - .SetCollectionId(68844) // Set the collection id. - .SetParams(burnParams); // Set the burn params. - -// Define and assign the return data fragment to the mutation -var burnFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -burn.Fragment(burnFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendBurn(burn); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -Snippet In Progress -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation BurnToken( - $collection_id: BigInt! - $token_id: BigInt! - $amount: BigInt! - ){ - Burn( - collectionId: $collection_id - params: { - tokenId: {integer: $token_id} - amount: $amount - } - ) { - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 5, //Specify the amount of supply to burn - amount: 1 //Specify the amount of supply to burn - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation BurnToken( - $collection_id: BigInt! - $token_id: BigInt! - $amount: BigInt! - ){ - Burn( - collectionId: $collection_id - params: { - tokenId: {integer: $token_id} - amount: $amount - } - ) { - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 5, //Specify the amount of supply to burn - amount: 1 //Specify the amount of supply to burn - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation BurnToken( - $collection_id: BigInt! - $token_id: BigInt! - $amount: BigInt! -){ - Burn( - collectionId: $collection_id - params: { - tokenId: {integer: $token_id} - amount: $amount - } - ) { - id - method - state - } -} -''' - -variables = { - 'collection_id': 36105, #Specify the collection ID - 'token_id': 5, #Specify the amount of supply to burn - 'amount': 1 #Specify the amount of supply to burn -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -Once the transaction is executed, the token supply will be burned - -### Destroying a token and removing it from the Blockchain - -:::info To destroy a token, these requirements must be met: -- The caller is the collection owner -- The token has no attributes - - If the token has attributes, they can be removed using the `RemoveAllAttributes` mutation -- The token has 0 supply - - You can remove the supply and destroy the token in the same Burn transaction. -::: - -Use the `Burn` mutation, and add `removeTokenStorage: true` property - - - -```graphql -mutation DestroyToken{ - Burn( - collectionId: 68844 #Specify the Collection ID - params: { - tokenId: {integer: 0} #Specify the Token ID - amount: 1 #Specify the amount of supply to burn - removeTokenStorage: true - } - ) { - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation BurnToken(\r\n $collection_id: BigInt!\r\n $token_id: BigInt!\r\n $amount: BigInt!\r\n) {\r\n Burn(\r\n collectionId: $collection_id\r\n params: {\r\n tokenId: { integer: $token_id }\r\n amount: $amount\r\n removeTokenStorage: true\r\n }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105,"token_id":5,"amount":1}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Set up the burn params -var burnParams = new BurnParamsInput() - .SetTokenId(new EncodableTokenIdInput().SetInteger(0)) // Set the token id. - .SetAmount(1) // Set the amount to burn. - .SetRemoveTokenStorage(true); // Set whether the token storage will be removed if no tokens are left. - -// Set up the mutation -var burn = new Burn() - .SetCollectionId(68844) // Set the collection id. - .SetParams(burnParams); // Set the burn params. - -// Define and assign the return data fragment to the mutation -var burnFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -burn.Fragment(burnFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendBurn(burn); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -Snippet In Progress -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation BurnToken( - $collection_id: BigInt! - $token_id: BigInt! - $amount: BigInt! - ){ - Burn( - collectionId: $collection_id - params: { - tokenId: {integer: $token_id} - amount: $amount - removeTokenStorage: true - } - ) { - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 5, //Specify the amount of supply to burn - amount: 1 //Specify the amount of supply to burn - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation BurnToken( - $collection_id: BigInt! - $token_id: BigInt! - $amount: BigInt! - ){ - Burn( - collectionId: $collection_id - params: { - tokenId: {integer: $token_id} - amount: $amount - removeTokenStorage: true - } - ) { - id - method - state - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 5, //Specify the amount of supply to burn - amount: 1 //Specify the amount of supply to burn - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation BurnToken( - $collection_id: BigInt! - $token_id: BigInt! - $amount: BigInt! -){ - Burn( - collectionId: $collection_id - params: { - tokenId: {integer: $token_id} - amount: $amount - removeTokenStorage: true - } - ) { - id - method - state - } -} -''' - -variables = { - 'collection_id': 36105, #Specify the collection ID - 'token_id': 5, #Specify the amount of supply to burn - 'amount': 1 #Specify the amount of supply to burn -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -Once the transaction is executed, the token will be destroyed and the will be retrieved. - -### Destroying a collection - -:::info To destroy a collection, these requirements must be met: -- The caller is the collection owner -- The collection has no attributes - - If the collection has attributes, they can be removed using the `RemoveAllAttributes` mutation -- The collection has 0 tokens **in storage** - - If the collection has some tokens, you can the above instructions for [Destroying a token](#destroying-a-token-and-removing-it-from-the-blockchain-1) for each of the tokens in the collection, to destroy them all. -::: - - - -```graphql -mutation DestroyCollection { - DestroyCollection( - collectionId: 68844 #Specify the Collection ID - ) { - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation DestroyCollection($collection_id: BigInt!) {\r\n DestroyCollection(collectionId: $collection_id) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":36105}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Set up the mutation -var destroyCollection = new DestroyCollection() - .SetCollectionId(68844); // Set the collection id. - -// Define and assign the return data fragment to the mutation -var destrotCollectionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -destroyCollection.Fragment(destrotCollectionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendDestroyCollection(destroyCollection); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -Snippet In Progress -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation DestroyCollection($collection_id: BigInt!){ - DestroyCollection( - collectionId: $collection_id - ) { - id - method - state - } - } - `, - variables: { - collection_id: 36105 //Specify the collection ID - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation DestroyCollection($collection_id: BigInt!){ - DestroyCollection( - collectionId: $collection_id - ) { - id - method - state - } - } - `, - variables: { - collection_id: 36105 //Specify the collection ID - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation DestroyCollection($collection_id: BigInt!){ - DestroyCollection( - collectionId: $collection_id - ) { - id - method - state - } -} -''' - -variables = { - 'collection_id': 36105 #Specify the collection ID -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -Once the transaction is executed, the collection will be destroyed. \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md deleted file mode 100644 index 13a5607..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md +++ /dev/null @@ -1,670 +0,0 @@ ---- -title: "Fetching Token Holders" -slug: "fetching-token-holders" -description: "Fetch token and collection holders using the Enjin Platform API." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -This guide explains how to fetch the current holders of a specific token or the owners of a collection using the Enjin Platform API. - -Accessing this data is valuable for several purposes: -- **Identify Token Holders:** Get a list of addresses that hold your tokens or own items from your collection. -- **Targeted Actions:** Use the list to perform actions specifically for your holders or owners, such as distributing rewards or communicating directly. -- **Analyze Distribution:** Understand how your tokens or collection items are distributed among users. - -:::info What you'll need: -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -::: - -## Fetching Token Holders with the [Enjin API](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) - -:::warning Fetching Tokens Limitations -Please note that the `GetToken` query is limited to tokens from collections that were created via the auth-ed Enjin Platform account. -To get a token that was created elsewhere (via a different Enjin Platform account / [NFT.io](https://nft.io) / [Enjin Console](https://console.enjin.io) / etc.) the collection must be "Tracked" first, or the query response will return an error. -Learn more about tracking a collection in the [Tracking Collections section below](#tracking-collections). -::: - -To get holders of a specific token, we use the `GetToken` query and include the the `accounts` in the query response: - -**Query:** - - - -```graphql -query FetchingTokenHolders{ - GetToken( - collectionId: 36105 #Specify the collection ID - tokenId: {integer: 0} #Specify the token ID - ){ - accounts{ - edges{ - node{ - balance - wallet{ - account{ - address - } - } - } - } - } - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query FetchingTokenHolders($collection_id: BigInt!, $token_id: BigInt!) {\r\n GetToken(collectionId: $collection_id, tokenId: { integer: $token_id }) {\r\n accounts {\r\n edges {\r\n node {\r\n balance\r\n wallet {\r\n account {\r\n address\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}","variables":{"collection_id":36105,"token_id":0}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the query -var getToken = new GetToken() - .SetCollectionId(36105) - .SetTokenId(new EncodableTokenIdInput().SetInteger(0)); - -// Define and assign the return data fragment to the query -var tokenFragment = new TokenFragment() - .WithAccounts(new ConnectionFragment() - .WithEdges(new EdgeFragment() - .WithNode(new TokenAccountFragment() - .WithBalance() - .WithWallet(new WalletFragment() - .WithAccount(new AccountFragment() - .WithAddress() - ) - ) - ) - ) - ); - -getToken.Fragment(tokenFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendGetToken(getToken); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreQueries.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Setup an Encodable Token ID - EncodableTokenIdInput tokenId = EncodableTokenIdInput() - .SetInteger(make_shared("0")); - - // Set up the query - GetToken getToken = GetToken() - .SetCollectionId(make_shared("36105")) - .SetTokenId(make_shared(tokenId)); - - // Define and assign the return data fragments to the query - AccountFragment accountFragment = AccountFragment() - .WithAddress(); - WalletFragment walletFragment = WalletFragment() - .WithAccount(make_shared(accountFragment)); - TokenAccountFragment tokenAccountFragment = TokenAccountFragment() - .WithBalance() - .WithWallet(make_shared(walletFragment)); - EdgeFragment edgeFragment = EdgeFragment() - .WithNode(make_shared(tokenAccountFragment)); - ConnectionFragment connectionFragment = ConnectionFragment() - .WithEdges(make_shared>(edgeFragment)); - - TokenFragment tokenFragment = TokenFragment() - .WithAccounts(make_shared>(connectionFragment)); - - getToken.SetFragment(make_shared(tokenFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendGetToken(*client, getToken); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& getTokenResult = gqlResult->GetData()->GetResult(); - - std::cout << getTokenResult->GetMinimumBalance().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query FetchingTokenHolders - ( - $collection_id: BigInt! - $token_id: BigInt! - ) { - GetToken( - collectionId: $collection_id - tokenId: {integer: $token_id} - ){ - accounts{ - edges{ - node{ - balance - wallet{ - account{ - address - } - } - } - } - } - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0 //Specify the token ID - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - query FetchingTokenHolders - ( - $collection_id: BigInt! - $token_id: BigInt! - ) { - GetToken( - collectionId: $collection_id - tokenId: {integer: $token_id} - ){ - accounts{ - edges{ - node{ - balance - wallet{ - account{ - address - } - } - } - } - } - } - } - `, - variables: { - collection_id: 36105, //Specify the collection ID - token_id: 0 //Specify the token ID - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -query FetchingTokenHolders -( - $collection_id: BigInt! - $token_id: BigInt! -) { - GetToken( - collectionId: $collection_id - tokenId: {integer: $token_id} - ){ - accounts{ - edges{ - node{ - balance - wallet{ - account{ - address - } - } - } - } - } - } -} -''' - -variables = { - 'collection_id': 36105, #Specify the collection ID - 'token_id': 0 #Specify the token ID -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -**Response:** - -```json -{ - "data": { - "GetToken": { - "accounts": { - "edges": [ - { - "node": { - "balance": "4", - "wallet": { - "account": { - "address": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" - } - } - } - }, - { - "node": { - "balance": "2", - "wallet": { - "account": { - "address": "cxJXQKFB4ZWJzutwEtZvTQ9KLucdsaWgzyef5pv9R7x98Dmwp" - } - } - } - } - ] - } - } - } -} -``` - -:::tip Using Pagination -The response may be displayed on several pages. To view all of it, you may need to follow steps for [pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination) which allows you to flip through the pages. -::: - -## Fetching Collection Holders with the [Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) - -:::warning Fetching Collections Limitations -Please note that the `GetCollections` query is limited to collections that were created via the auth-ed Enjin Platform Cloud account. -To get a collection that was created elsewhere (via a different Enjin Platform Cloud account / [NFT.io](https://nft.io) / [Enjin Console](https://console.enjin.io) / etc.) the collection must be "Tracked" first, or the query response will return an error. -Learn more about tracking a collection in the [Tracking Collections section below](#tracking-collections). -::: - -For an [NFT collection](/02-guides/01-platform/03-advanced-mechanics/03-enforced-rarity.md#nft-collection), we can use the `GetCollection` query and include the the `accounts` in the query response to find out how many NFTs from the collection are owned by each address. - -**Query:** - - - -```graphql -query FetchingCollectionHolders{ - GetCollection(collectionId: 36105){ #Specify the collection ID - accounts{ - edges{ - node{ - accountCount - wallet{ - account{ - address - } - } - } - } - } - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query FetchingCollectionHolders($collection_id: BigInt!) {\r\n GetCollection(collectionId: $collection_id) {\r\n accounts {\r\n edges {\r\n node {\r\n accountCount\r\n wallet {\r\n account {\r\n address\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}","variables":{"collection_id":36105}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the query -var getCollection = new GetCollection() - .SetCollectionId(36105); - -// Define and assign the return data fragment to the query -var collectionFragment = new CollectionFragment() - .WithAccounts(new ConnectionFragment() - .WithEdges(new EdgeFragment() - .WithNode(new CollectionAccountFragment() - .WithAccountCount() - .WithWallet(new WalletFragment() - .WithAccount(new AccountFragment() - .WithAddress() - ) - ) - ) - ) - ); - -getCollection.Fragment(collectionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendGetCollection(getCollection); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); CC -``` - - -```cpp -#include "EnjinPlatformSdk/CoreQueries.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Set up the query - GetCollection getCollection = GetCollection() - .SetCollectionId(make_shared("36105")); - - // Define and assign the return data fragments to the query - CollectionAccountFragment collectionAccountFragment = CollectionAccountFragment() - .WithAccountCount() - .WithWallet(make_shared(WalletFragment() - .WithAccount(make_shared(AccountFragment() - .WithAddress() - ) - ) - ) - ); - - EdgeFragment edgeFragment = EdgeFragment() - .WithNode(make_shared(collectionAccountFragment)); - - ConnectionFragment connectionFragment = ConnectionFragment() - .WithEdges(make_shared>(edgeFragment)); - - CollectionFragment collectionFragment = CollectionFragment() - .WithAccounts(make_shared>(connectionFragment)); - - - getCollection.SetFragment(make_shared(collectionFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendGetCollection(*client, getCollection); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& getCollectionResult = gqlResult->GetData()->GetResult(); - - std::cout << getCollectionResult->GetCollectionId().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query FetchingCollectionHolders - ( - $collection_id: BigInt! - ) { - GetCollection(collectionId: $collection_id){ - accounts{ - edges{ - node{ - accountCount - wallet{ - account{ - address - } - } - } - } - } - } - } - `, - variables: { - collection_id: 36105 //Specify the collection ID - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - query FetchingCollectionHolders - ( - $collection_id: BigInt! - ) { - GetCollection(collectionId: $collection_id){ - accounts{ - edges{ - node{ - accountCount - wallet{ - account{ - address - } - } - } - } - } - } - } - `, - variables: { - collection_id: 36105 //Specify the collection ID - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -query FetchingCollectionHolders -( - $collection_id: BigInt! -) { - GetCollection(collectionId: $collection_id){ - accounts{ - edges{ - node{ - accountCount - wallet{ - account{ - address - } - } - } - } - } - } -} -''' - -variables = { - 'collection_id': 36105 #Specify the collection ID -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -**Response:** - -```json -{ - "data": { - "GetCollection": { - "accounts": { - "edges": [ - { - "node": { - "accountCount": 3, - "wallet": { - "account": { - "address": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" - } - } - } - }, - { - "node": { - "accountCount": 2, - "wallet": { - "account": { - "address": "cxJXQKFB4ZWJzutwEtZvTQ9KLucdsaWgzyef5pv9R7x98Dmwp" - } - } - } - } - ] - } - } - } -} -``` - -:::tip Using Pagination -The response may be displayed on several pages. To view all of it, you may need to follow steps for [pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination) which allows you to flip through the pages. -::: - -:::info Explore More Arguments -For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -::: - -## Tracking Collections - -The `GetToken`, `GetTokens`, `GetCollection`, and `GetCollections` queries are limited only to collections and tokens that were created via the auth-ed Enjin Platform account. To use these queries with collections / tokens created externally (via a different Enjin Platform account / [NFT.io](https://nft.io) / [Enjin Console](https://console.enjin.io) / etc.) the collection must be "Tracked" first, or the query response will return an error. - -To track a collection, head over to the collections page on Enjin Platform: https://platform.enjin.io/collections, click on the "Track" button, insert the collection ID of the collection you want to track and click "Track". - -![Track Collection](/img/guides/managing-tokens/track-collection.png) - -:::tip Need to track collections programmatically? -You can use the `AddToTracked` mutation to track a collection programmatically. -::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/_category_.json deleted file mode 100644 index 7536ec8..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/01-managing-tokens/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Managing Tokens" -} - \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md deleted file mode 100644 index f5e1df5..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md +++ /dev/null @@ -1,237 +0,0 @@ ---- -title: "Using WalletConnect" -slug: "using-wallet-connect" -description: "Find out how to use WalletConnect to seamlessly connect user wallets to your application, enhancing blockchain integration and accessibility." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -[WalletConnect](https://docs.walletconnect.com/) is an open-source protocol that connects your mobile wallet to decentralized applications. -Using the Platform API and WalletConnect you can send any transaction request directly to your user's wallet app. - -:::danger **Maintenance Notice** -The `polkadot-onboard` sample repository used in this guide is **no longer maintained** and may not function as expected. - -**Note:** WalletConnect itself remains fully functional and compatible with Enjin Blockchain. This guide is retained as a reference for the implementation logic for developers building their own integrations. - -**Upcoming Feature:** A native solution is in development to enable direct transaction requests from the Enjin Platform to the Enjin Wallet, removing the need for third-party services. -::: - -:::info What you'll need: -- Node.js 18+ and yarn installed -- A mobile device with [Enjin Wallet app](https://enjin.io/wallet) installed -- A Canary wallet with at least 6.5 ENJ in it to cover for and . -You can obtain cENJ for testing from the [Canary faucet](https://faucet.canary.enjin.io/). -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md) (Canary). -::: - -## Install and configure the sample project - -On production apps, you will need [WalletConnect's SDK](https://docs.walletconnect.com/web3wallet/about), but for this guide we'll be using the (now deprecated) sample project called [polkadot-onboard](https://github.com/enjin-forks/polkadot-onboard.git). - -Start by cloning the polkadot-onboard repository: https://github.com/enjin-forks/polkadot-onboard.git - -Once cloned, install the packages, install the workspace, build the workspace, and start the example project. -To do that, navigate to the cloned repository folder and run the following commands: - -:::info **Note:** -The commands below are Bash commands, which are commonly run in a Unix-like environment (such as macOS, or Linux). -Windows users may encounter errors if they try running these commands directly in Command Prompt or PowerShell. -To run them successfully on Windows, use a Bash-compatible terminal, like [Git Bash](https://gitforwindows.org/). -::: - -```bash -yarn -yarn install:workspace -yarn build:workspace -``` - -Now, let's run the project and test it. You can do that by running the following command inside the root folder of the project: - -```bash -yarn start:react-next -``` - -Navigate to http://localhost:3000 using your browser. If everything went well, a sample page should load up. - -:::info Configured Chain -The sample project is set to broadcast transaction on the Canary Matrixchain Testnet. -To configure a different blockchain, you can adjust the `chainIds` in the `examples/react-next/components/ConnectContainer.tsx` file. - -Chain IDs on Enjin Chains: -- Mainnet Matrixchain: `polkadot:3af4ff48ec76d2efc8476730f423ac07` -- Mainnet Relaychain: `polkadot:d8761d3c88f26dc12875c00d3165f7d6` -- Canary Matrixchain: `polkadot:a37725fd8943d2a524cb7ecc65da438f` -- Canary Relaychain: `polkadot:735d8773c63e74ff8490fee5751ac07e` -::: - -## Connect user's wallet - -To send transactions to your user, he must be connected to your dapp. -Clicking `Get Wallets`, followed by `Polkadot Demo 2.0`, initiates a connection between your user and your dapp. - -

- Connect Wallet Modal -

- -Once the user scans the QR code with his Enjin wallet app (or any other wallet with WalletConnect & Enjin Blockchain support) he will be asked to approve the connection: - -

- Approve Connection Request -

- -Once a connection was made, you'll receive a list of all addresses connected from your user's wallet. -Now you are able to send any transaction request to your user's wallet. -To demonstrate a sample transaction request, click the "**Submit Transaction with Polkadot.JS**" button on one of the displayed addresses. - -

- Connected Wallets -

- -A Transaction Request dialog will pop up on user's connected wallet app, asking for approval. - -

- Confirm Transaction Request -

- -Once the transaction request is confirmed by the user, his wallet will sign and broadcast a message saying "I am signing this transaction!". - -In the next section, you'll learn how to construct your own transaction requests using Enjin Platform API - -## How to construct a custom transaction? - -In the example above, we have sent a request to broadcast a transaction that was already constructed for us. -But how was that transaction constructed and how can we send any transaction we want? Let's find out! - -At `examples/react-next/pages/api`, there's two endpoints we'll be using: - -- `transaction.tsx` endpoint which constructs the transaction -- `send.tsx` which broadcasts the transaction request to the blockchain using Enjin Platform API. - -First, we need to insert our Enjin platform API key to be able to interact with it. -If you haven't created an api key yet, you can do so in https://platform.canary.enjin.io/settings. - -In both `examples/react-next/pages/api/transaction.tsx` and `examples/react-next/pages/api/send.tsx` files, replace `your-api-key` with your api key (note, there's three different api key fields that needs to be updated). - -Once we've configured the Enjin Platform API key, rebuild and restart the project: - -:::info **Note:** -The commands below are Bash commands, which are commonly run in a Unix-like environment (such as macOS, or Linux). -Windows users may encounter errors if they try running these commands directly in Command Prompt or PowerShell. -To run them successfully on Windows, use a Bash-compatible terminal, like [Git Bash](https://gitforwindows.org/). -::: - -```bash -yarn build:workspace -yarn start:react-next -``` - -Now that the project is authenticated with the Enjin Platform, we can go ahead and press the "**Submit Transaction with Platform**" button. -A `CreateCollection` request will be sent to your wallet, and once confirmed, the transaction will be signed and broadcasted to the blockchain. -Now it's time to break it down and understand how it works: - -### Step #1: Constructing the transaction call - -We are constructing the transaction using the `transaction.tsx` endpoint. -First, we make a query to the user's address to get its nonce: - -```javascript - const walletResponse = await fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - mode: 'no-cors', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'my-auth-key', - }, - body: JSON.stringify({ - query: `query GetWallet($account: String!) { - GetWallet(account: $account) { - nonce - } - }`, - variables: { - account: req.query.address - } - }) - }); -``` - -Once we get the nonce, we prepare the `CreateCollection` transaction call using Enjin Platform API. -You can use any other mutation Enjin Platform API offers, but for this example we're using CreateCollection. - -```javascript - const response = await fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - mode: 'no-cors', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'my-auth-key', - }, - body: JSON.stringify({ - query: `mutation CreateCollection($nonce: Int!, $signingAccount: String!) { - CreateCollection( - mintPolicy: { forceSingleMint: false }, - signingAccount: $signingAccount - ) { - id - signingPayloadJson(nonce: $nonce) - } - }`, - variables: { - signingAccount: req.query.address, - nonce: nonce - } - }) - }); -``` - -We are also passing the `signingAccount`. This is necessary so your daemon doesn't sign the transaction. -In the mutation response, we're asking for the `id` and `signingPayloadJson`, which we will use in the next step. - -### Step #2: Sending the Transaction Request to user's wallet - -With the payload in hand we pass that to our WalletConnect signer and ask it to sign. In the example project, this is done at: `examples/react-next/components/AccountBox.tsx` - -```javascript -const txId = data?.data?.CreateCollection?.id; -const payload = data?.data?.CreateCollection?.signingPayloadJson; -const { signature } = await signer?.signPayload(payload) -``` - -The above will trigger the Transaction Request dialog you saw previously, asking for the user to confirm signing the transaction. - -### Step #3: Broadcasting the signed transaction - -Finally, we received the `txId`, `signature`, and `payload` from the user's wallet. -We can now broadcast the signed transaction to the blockchain using the Enjin Platform API `SendTransaction` mutation. -This is done at: `examples/react-next/pages/api/send.tsx` - -```javascript - const response = await fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - mode: 'no-cors', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'my-auth-key', - }, - body: JSON.stringify({ - query: `mutation SendTransaction($id: Int!, $signingPayloadJson: Object!, $signature: String!) { - SendTransaction(id: $id, signingPayloadJson: $signingPayloadJson, signature: $signature) - }`, - variables: { - id: Number.parseInt(req.query.id), - signingPayloadJson: JSON.parse(req.query.signingPayloadJson), - signature: req.query.signature - } - }) - }); -``` - -The Enjin Platform will broadcast the transaction to the blockchain and keep track of it. -You can see the transaction status in Enjin Platform: https://platform.canary.enjin.io/transactions - -*** - -**Congratulations!** you can now send any transaction request to your user's wallets. diff --git a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md deleted file mode 100644 index 4fc55c4..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/02-verifying-wallets.md +++ /dev/null @@ -1,717 +0,0 @@ ---- -title: "Verifying Wallets" -slug: "verifying-wallets" -description: "Learn how to verify user wallets to ensure secure and trustworthy transactions within the Enjin platform, enhancing security and reliability." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Before you can interact with a user's blockchain items or associate their in-app identity with their blockchain address, you need a reliable way to confirm they own the wallet they provide. The Enjin Platform's wallet verification process provides a secure method to achieve this. - -By verifying a user's wallet, you gain certainty that the public wallet address shared genuinely belongs to them. This verified link is essential for securely accessing their blockchain inventory, delivering items, or granting ownership-based perks within your application. - -The verification process typically involves: - -1. Your application requests a unique verification code and QR code from the Enjin API. -2. The user scans the QR code with their . -3. The confirms ownership and securely provides the wallet address to the Enjin Platform. -4. Your application retrieves the verified address from the Platform. - -Once verified, you can confidently associate this blockchain address with the user's account in your system, enabling seamless blockchain interactions. - -:::tip -If you need to send requests to user wallets, use [WalletConnect](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md) instead. -::: - -:::info What you'll need: -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -::: - -## Verify Wallet with the [Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) - -### Step 1. Initiate an Account Request - -To initiate the account request process, run the following query: - -**Query:** - - - -```graphql -query RequestAccount{ - RequestAccount{ - qrCode #Returns a QR code for your user to scan - verificationId #Save this for next step - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query RequestAccount {\r\n RequestAccount {\r\n qrCode #Returns a QR code for your user to scan\r\n verificationId #Save this for next step\r\n }\r\n}\r\n","variables":{}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the query -var requestAccount = new RequestAccount(); - -// Define and assign the return data fragment to the query -var accountRequestFragment = new AccountRequestFragment() - .WithQrCode() - .WithVerificationId(); - -requestAccount.Fragment(accountRequestFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendRequestAccount(requestAccount); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreQueries.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Set up the query - RequestAccount requestAccount = RequestAccount(); - - // Define and assign the return data fragments to the query - AccountRequestFragment accountRequestFragment = AccountRequestFragment() - .WithQrCode() - .WithVerificationId(); - - requestAccount.SetFragment(make_shared(accountRequestFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendRequestAccount(*client, requestAccount); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& getAccountRequestResult = gqlResult->GetData()->GetResult(); - - std::cout << getAccountRequestResult->GetVerificationId().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query RequestAccount{ - RequestAccount{ - qrCode #Returns a QR code for your user to scan - verificationId #Save this for next step - } - } - ` - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - query RequestAccount{ - RequestAccount{ - qrCode #Returns a QR code for your user to scan - verificationId #Save this for next step - } - } - ` -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = `''' -query RequestAccount{ - RequestAccount{ - qrCode #Returns a QR code for your user to scan - verificationId #Save this for next step - } -} -'''` - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -**Response:** - -```json -{ - "data": { - "RequestAccount": { - "qrCode": "https://platform.canary.enjin.io/qr?s=512&f=png&d=https://platform.canary.enjin.io/proof/MHg3OGFmYWIxYWQxMjk0YzFlZWY4MjdlNmFjNTM1MmJiOTVmNmFmNWJhNmUyNjk5ZGRkOTI3YjAwNmQ3ZDk0MzZjO2Vwc3I6QTIwMTBBSzlM", - "verificationId": "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" - } - } -} -``` - -This will return a link to a QR Code for you to display to your user, and a unique verification id to track the request. -In your application, you'll display this QR code to your user and instruct them to scan it with their . - -### Step 2: Verify Account - -To confirm that the user scanned the QR code and approved the request, you can retrieve the account using two methods. - -#### Option A: `GetWallet` - -Using `GetWallet` and passing the `verificationId` from step 1, you will be able to get any information related to his wallet if the verification process succeeded. - -**Query:** - - - -```graphql -query GetVerifiedWallet{ - GetWallet(verificationId: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"){ #Set the verificationId from the RequestAccount response - account{ - address - } - balances{ - free - } - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query GetVerifiedWallet($verification_id: String!) {\r\n GetWallet(verificationId: $verification_id) {\r\n account {\r\n address\r\n }\r\n balances {\r\n free\r\n }\r\n }\r\n}","variables":{"verification_id":"0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the query -var getWallet = new GetWallet() - .SetVerificationId("0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"); - -// Define and assign the return data fragment to the query -var walletFragment = new WalletFragment() - .WithAccount(new AccountFragment() - .WithAddress() - ) - .WithBalances(new BalancesFragment() - .WithFree() - ); - -getWallet.Fragment(walletFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendGetWallet(getWallet); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); - -``` - - -```cpp -#include "EnjinPlatformSdk/CoreQueries.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Set up the query - GetWallet getWallet = GetWallet() - .SetVerificationId(make_shared("0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c")); - - // Define and assign the return data fragments to the query - AccountFragment accountFragment = AccountFragment() - .WithAddress(); - BalancesFragment balancesFragment = BalancesFragment() - .WithFree(); - WalletFragment walletFragment = WalletFragment() - .WithAccount(make_shared(accountFragment)) - .WithBalances(make_shared(balancesFragment)); - - getWallet.SetFragment(make_shared(walletFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendGetWallet(*client, getWallet); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& getWalletResult = gqlResult->GetData()->GetResult(); - - std::cout << getWallet.GetName() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} - -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query GetVerifiedWallet($verification_id: String!) { - GetWallet(verificationId: $verification_id) { - account{ - address - } - balances{ - free - } - } - } - `, - variables: { - verification_id: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" //Set the verificationId from the RequestAccount response - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - query GetVerifiedWallet($verification_id: String!) { - GetWallet(verificationId: $verification_id) { - account{ - address - } - balances{ - free - } - } - } - `, - variables: { - verification_id: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" //Set the verificationId from the RequestAccount response - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -query GetVerifiedWallet($verification_id: String!) { - GetWallet(verificationId: $verification_id) { - account{ - address - } - balances{ - free - } - } -} -''' - -variables = { - 'verification_id': "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" #Set the verificationId from the RequestAccount response -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -**Response: If user has verified** - -```json -{ - "data": { - "GetWallet": { - "account": { - "address": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //The address of the verified account - }, - "balances": { - "free": "86010842630734264894" // The amount of free ENJ in the verified account - ~86.0108 ENJ - } - } - } -} -``` - -:::info **Formatting Balances** -The API returns balance fields in the **base unit** (u128), meaning they appear as large integers without decimals. To obtain the readable ENJ amount, **divide the returned value by 10^18**. -- **Formula:** `Base Unit Value` / `1,000,000,000,000,000,000` = `ENJ Amount` -- **Example:** A value of `86010842630734264894` converts to approximately **86.01 ENJ**. -::: - -**Response: If user has not verified** - -```json -{ - "data": { - "GetWallet": null - } -} -``` - -#### Option B: `GetAccountVerified` - -This query can be used to fetch the status of the verification, while you are showing the QR code to the player, for example, when the query returns `"verified": true` you could hide the QR code and proceed with your onboarding workflow. - -**Query:** - - - -```graphql -query GetAccountVerified{ - GetAccountVerified(verificationId: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"){ #Set the verificationId from the RequestAccount response - verified - account{ - publicKey - address - } - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query GetAccountVerified($verification_id: String!) {\r\n GetAccountVerified(verificationId: $verification_id) {\r\n verified\r\n account {\r\n publicKey\r\n address\r\n }\r\n }\r\n}","variables":{"verification_id":"0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the query -var getAccountVerified = new GetAccountVerified() - .SetVerificationId("0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c"); - -// Define and assign the return data fragment to the query -var accountVerifiedFragment = new AccountVerifiedFragment() - .WithVerified() - .WithAccount(new AccountFragment() - .WithPublicKey() - .WithAddress() - ); - -getAccountVerified.Fragment(accountVerifiedFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendGetAccountVerified(getAccountVerified); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreQueries.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Set up the query - GetAccountVerified getAccountVerified = GetAccountVerified() - .SetVerificationId(make_shared("0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c")); - - // Define and assign the return data fragments to the query - AccountFragment accountFragment = AccountFragment() - .WithPublicKey() - .WithAddress(); - AccountVerifiedFragment accountVerifiedFragment = AccountVerifiedFragment() - .WithVerified() - .WithAccount(make_shared(accountFragment)); - - getAccountVerified.SetFragment(make_shared(accountVerifiedFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendGetAccountVerified(*client, getAccountVerified); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& getAccountVerifiedResult = gqlResult->GetData()->GetResult(); - - std::cout << getAccountVerifiedResult->GetVerified().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} - -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query GetAccountVerified($verification_id: String!) { - GetAccountVerified(verificationId: $verification_id) { - verified - account{ - publicKey - address - } - } - } - `, - variables: { - verification_id: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" //Set the verificationId from the RequestAccount response - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - query GetAccountVerified($verification_id: String!) { - GetAccountVerified(verificationId: $verification_id) { - verified - account{ - publicKey - address - } - } - } - `, - variables: { - verification_id: "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" //Set the verificationId from the RequestAccount response - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -query GetAccountVerified($verification_id: String!) { - GetAccountVerified(verificationId: $verification_id) { - verified - account{ - publicKey - address - } - } -} -''' - -variables = { - 'verification_id': "0x78afab1ad1294c1eef827e6ac5352bb95f6af5ba6e2699ddd927b006d7d9436c" #Set the verificationId from the RequestAccount response -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -**Response: If user has verified** - -```json -{ - "data": { - "GetAccountVerified": { - "verified": true, - "account": { - "publicKey": "0x5a6aae294416f3e875d9a8975658905002cfd3e5e64105d76296c4b0adbfd77e", //The public key of the verified account - "address": "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //The address of the verified account - } - } - } -} -``` - -**Response: If user has not verified** - -```json -{ - "data": { - "GetAccountVerified": { - "verified": false, - "address": null - } - } -} -``` - -![Verify Flow Diagram](/img/guides/managing-users/verify-flow-diagram.png) - -:::info Explore More Arguments -For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/_category_.json deleted file mode 100644 index 7c7d976..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/_category_.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "label": "Connecting User Wallets" -} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md deleted file mode 100644 index 687e483..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md +++ /dev/null @@ -1,915 +0,0 @@ ---- -title: "Reading User Wallets" -slug: "reading-user-wallets" -description: "Explore how to read user wallet data, providing insights into user transactions, holdings, and interactions within the blockchain." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Fetching wallets is crucial because it allows you to see the contents of a wallet, including all tokens held within. - -This is especially important in gaming and app development as it enables you to assign specific utilities or benefits to certain tokens. - -For example, if a user has a particular token in their wallet, they might gain access to exclusive in-game items or features within an app, enhancing the user experience and adding value to the tokens. - -:::info What you'll need: -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -::: - -:::tip Best Practices -- Only pull data from the collections you need. This will improve performance and ensure your backend infrastructure remains scalable. -- Always read and check the balance. Sometimes, the player may appear to hold the token but the balance is actually zero, for instance, if the token has been burned. These burned tokens should not have in-game utility. -- Consider whether you want NFTs that are listed on a marketplace to have utility in your game. Items that are listed for sale don't show up in regular token balance and show up in reserved balance instead. If you want them to have utility, you can check the reserved balance and include the reserved supply. -- When an API call response contains too many results, they are paginated. Users can have hundreds of tokens, in this situation you will need to read them in multiple calls. [Read more about Pagination here](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination). -- If you plan to utilize metadata from on-chain or external sources, it's important to also read the token's attributes. Typically, you'll find a "uri" attribute that points to the external location of this metadata. -::: - -## Fetching a wallet with the [Enjin API](/01-getting-started/05-using-enjin-api/05-using-enjin-api.md) - -### Fetching wallet's Enjin Coin balance - -Use the `GetWallet` query and include the the `balances` in the query response to see how much ENJ a wallet holds. - -**Query:** - - - -```graphql -query FetchWalletBalance{ - GetWallet(account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"){ #Specify the account address - balances{ - free - reserved - } - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query FetchWalletBalance($account: String!) {\r\n GetWallet(account: $account) {\r\n balances {\r\n free\r\n reserved\r\n }\r\n }\r\n}","variables":{"account":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the query -var getWallet = new GetWallet() - .SetAccount("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"); - -// Define and assign the return data fragment to the query -var walletFragment = new WalletFragment() - .WithBalances(new BalancesFragment() - .WithFree() - .WithReserved() - ); - -getWallet.Fragment(walletFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendGetWallet(getWallet); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreQueries.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Set up the query - GetWallet getWallet = GetWallet() - .SetAccount(make_shared("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f")); - - // Define and assign the return data fragments to the query - WalletFragment walletFragment = WalletFragment(); - BalancesFragment balancesFragment = BalancesFragment() - .WithFree() - .WithReserved(); - - walletFragment.WithBalances(make_shared(balancesFragment)); - - getWallet.SetFragment(make_shared(walletFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendGetWallet(*client, getWallet); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& getWalletResult = gqlResult->GetData()->GetResult(); - - std::cout << getWalletResult->GetBalances()->GetFree().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query FetchWalletBalance($account: String!) { - GetWallet(account: $account) { - balances{ - free - reserved - } - } - } - `, - variables: { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - query FetchWalletBalance($account: String!) { - GetWallet(account: $account) { - balances{ - free - reserved - } - } - } - `, - variables: { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -query FetchWalletBalance($account: String!) { - GetWallet(account: $account) { - balances{ - free - reserved - } - } -} -''' - -variables = { - 'account': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the account address -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -**Response:** - -```json -{ - "data": { - "GetWallet": { - "balances": { - "free": "86010842630734264894", //~86.01084... ENJ - "reserved": "13900475000000000000" //~13.90047... ENJ - } - } - } -} -``` - -:::info **Formatting Balances** -The API returns balance fields in the **base unit** (u128), meaning they appear as large integers without decimals. To obtain the readable ENJ amount, **divide the returned value by 10^18**. -- **Formula:** `Base Unit Value` / `1,000,000,000,000,000,000` = `ENJ Amount` -- **Example:** A value of `86010842630734264894` converts to approximately **86.01 ENJ**. -::: - -### Fetch a wallet's collections - -Use the `GetWallet` query and include the the `collectionAccounts` in the query response to see what collections a wallet holds. - -**Query:** - - - -```graphql -query FetchingWalletCollections{ - GetWallet(account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"){ #Specify the account address - collectionAccounts{ - edges{ - node{ - collection{ - collectionId - attributes{ - key - value - } - } - } - } - } - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query FetchingWalletCollections($account: String!) {\r\n GetWallet(account: $account) {\r\n collectionAccounts {\r\n edges {\r\n node {\r\n collection {\r\n collectionId\r\n attributes {\r\n key\r\n value\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}","variables":{"account":"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the query -var getWallet = new GetWallet() - .SetAccount("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"); - -// Define and assign the return data fragment to the query -var walletFragment = new WalletFragment() - .WithCollectionAccounts(new ConnectionFragment() - .WithEdges(new EdgeFragment() - .WithNode(new CollectionAccountFragment() - .WithCollection(new CollectionFragment() - .WithCollectionId() - .WithAttributes(new AttributeFragment() - .WithKey() - .WithValue() - ) - ) - ) - ) - ); - -getWallet.Fragment(walletFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendGetWallet(getWallet); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreQueries.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Set up the query - GetWallet getWallet = GetWallet() - .SetAccount(make_shared("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f")); - - // Define and assign the return data fragments to the query - AccountFragment accountFragment = AccountFragment() - .WithAddress(); - WalletFragment walletFragment = WalletFragment() - .WithAccount(make_shared(accountFragment)); - AttributeFragment attributeFragment = AttributeFragment() - .WithKey() - .WithValue(); - CollectionFragment collectionFragment = CollectionFragment() - .WithCollectionId() - .WithAttributes(make_shared(attributeFragment)); - CollectionAccountFragment collectionAccountFragment = CollectionAccountFragment() - .WithCollection(make_shared(collectionFragment)); - EdgeFragment edgeFragment = EdgeFragment() - .WithNode(make_shared(collectionAccountFragment)); - - ConnectionFragment connectionFragment = ConnectionFragment() - .WithEdges(make_shared>(edgeFragment)); - - walletFragment.WithCollectionAccounts(make_shared>(connectionFragment)); - - getWallet.SetFragment(make_shared(walletFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendGetWallet(*client, getWallet); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& getWalletResult = gqlResult->GetData()->GetResult(); - - std::cout << getWalletResult->GetAccount()->GetAddress().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query FetchingWalletCollections($account: String!) { - GetWallet(account: $account) { - collectionAccounts{ - edges{ - node{ - collection{ - collectionId - attributes{ - key - value - } - } - } - } - } - } - } - `, - variables: { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - query FetchingWalletCollections($account: String!) { - GetWallet(account: $account) { - collectionAccounts{ - edges{ - node{ - collection{ - collectionId - attributes{ - key - value - } - } - } - } - } - } - } - `, - variables: { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -query FetchingWalletCollections($account: String!) { - GetWallet(account: $account) { - collectionAccounts{ - edges{ - node{ - collection{ - collectionId - attributes{ - key - value - } - } - } - } - } - } -} -''' - -variables = { - 'account': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the account address -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -**Response:** - -```json -{ - "data": { - "GetWallet": { - "collectionAccounts": { - "edges": [ - { - "node": { - "collection": { - "collectionId": "33866", - "attributes": [] - } - } - }, - { - "node": { - "collection": { - "collectionId": "36105", - "attributes": [ - { - "key": "name", - "value": "My test collection" - } - ] - } - } - }, - { - "node": { - "collection": { - "collectionId": "36623", - "attributes": [ - { - "key": "name", - "value": "Docs Testing Collection" - } - ] - } - } - } - ] - } - } - } -} -``` - -:::tip Using Pagination -The response may be displayed on several pages. To view all of it, you may need to follow steps for [pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination) which allows you to flip through the pages. -::: - -### Fetch a wallet's tokens - -Use the `GetWallet` query and include the the `tokenAccounts` in the query response to see what tokens a wallet holds. - -**Query:** - - - -```graphql -query FetchingWalletTokens{ - GetWallet(account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"){ #Specify the account address - tokenAccounts{ - edges{ - node{ - balance - token{ - tokenId - collection{ - collectionId - } - attributes{ - key - value - } - } - } - } - } - } -} -``` - - -``` -curl -X POST "https://platform.enjin.io/graphql" \ --H "Content-Type: application/json" \ --H "Authorization: enjin_api_key" \ --d '{"query": "query FetchingWalletTokens { GetWallet(account: \"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f\") { tokenAccounts { edges { node { balance token { tokenId collection { collectionId } attributes { key value } } } } } } }"}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the query -var getWallet = new GetWallet() - .SetAccount("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"); - -// Define and assign the return data fragment to the query -var walletFragment = new WalletFragment() - .WithTokenAccounts(new ConnectionFragment() - .WithEdges(new EdgeFragment() - .WithNode(new TokenAccountFragment() - .WithBalance() - .WithToken(new TokenFragment() - .WithTokenId() - .WithCollection(new CollectionFragment() - .WithCollectionId() - ) - .WithAttributes(new AttributeFragment() - .WithKey() - .WithValue() - ) - ) - ) - ) - ); - -getWallet.Fragment(walletFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendGetWallet(getWallet); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreQueries.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Set up the query - GetWallet getWallet = GetWallet() - .SetAccount(make_shared("cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f")); - - // Define and assign the return data fragments to the query - AccountFragment accountFragment = AccountFragment() - .WithAddress(); - WalletFragment walletFragment = WalletFragment() - .WithAccount(make_shared(accountFragment)); - AttributeFragment attributeFragment = AttributeFragment() - .WithKey() - .WithValue(); - CollectionFragment collectionFragment = CollectionFragment() - .WithCollectionId(); - TokenFragment tokenFragment = TokenFragment() - .WithTokenId() - .WithCollection(make_shared(collectionFragment)) - .WithAttributes(make_shared(attributeFragment)); - TokenAccountFragment tokenAccountFragment = TokenAccountFragment() - .WithBalance() - .WithToken(make_shared(tokenFragment)); - - EdgeFragment edgeFragment = EdgeFragment() - .WithNode(make_shared(tokenAccountFragment)); - - ConnectionFragment connectionFragment = ConnectionFragment() - .WithEdges(make_shared>(edgeFragment)); - - walletFragment.WithTokenAccounts(make_shared>(connectionFragment)); - - getWallet.SetFragment(make_shared(walletFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendGetWallet(*client, getWallet); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& getWalletResult = gqlResult->GetData()->GetResult(); - - std::cout << getWalletResult->GetAccount()->GetAddress().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query FetchingWalletTokens($account: String!) { - GetWallet(account: $account) { - tokenAccounts{ - edges{ - node{ - balance - token{ - tokenId - collection{ - collectionId - } - attributes{ - key - value - } - } - } - } - } - } - } - `, - variables: { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - query FetchingWalletTokens($account: String!) { - GetWallet(account: $account) { - tokenAccounts{ - edges{ - node{ - balance - token{ - tokenId - collection{ - collectionId - } - attributes{ - key - value - } - } - } - } - } - } - } - `, - variables: { - account: "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" //Specify the account address - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -query FetchingWalletTokens($account: String!) { - GetWallet(account: $account) { - tokenAccounts{ - edges{ - node{ - balance - token{ - tokenId - collection{ - collectionId - } - attributes{ - key - value - } - } - } - } - } - } -} -''' - -variables = { - 'account': "cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f" #Specify the account address -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -**Response:** - -```json -{ - "data": { - "GetWallet": { - "tokenAccounts": { - "edges": [ - { - "node": { - "balance": "1", - "token": { - "tokenId": "0", - "collection": { - "collectionId": "33866" - }, - "attributes": [] - } - } - }, - { - "node": { - "balance": "1", - "token": { - "tokenId": "0", - "collection": { - "collectionId": "36105" - }, - "attributes": [ - { - "key": "Name", - "value": "Awesome Token!" - } - ] - } - } - }, - { - "node": { - "balance": "6", - "token": { - "tokenId": "1", - "collection": { - "collectionId": "36105" - }, - "attributes": [ - { - "key": "name", - "value": "Awesome Token 2!" - } - ] - } - } - } - ] - } - } - } -} -``` - -:::tip Using Pagination -The response may be displayed on several pages. To view all of it, you may need to follow steps for [pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination) which allows you to flip through the pages. -::: - -:::info Explore More Arguments -For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md deleted file mode 100644 index f6314e5..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md +++ /dev/null @@ -1,975 +0,0 @@ ---- -title: "Using Managed Wallets" -slug: "using-managed-wallets" -description: "Learn how to use managed wallets within the Enjin ecosystem, allowing for user-friendly blockchain wallet management and asset handling." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Managed wallets provide a seamless experience for your users by allowing you to create and manage blockchain wallets on their behalf. Users don't need to download any special apps or take extra steps, making it easier for them to get started with your application. - -This approach offers transparency, allowing users to easily track their digital assets. It simplifies user onboarding and provides clear, immutable on-chain data for tracking tokens, enhancing trust and engagement within your platform. - -Here's how the typical flow works: - -1. **[Managed Wallet Creation](#create-managed-wallets):** When a new user starts using your application, you initiate the creation of a managed wallet for them, and assign the user's internal ID from your database to the newly created managed wallet. This ensures a consistent link between the user and their wallet. -2. **[Asset Management](#minting-tokens-to-managed-wallets):** As the user earns or deposits digital assets within your application, these assets are automatically stored in their managed wallet. -3. **[In-App Transactions](#minting-tokens-to-managed-wallets):** Any actions the user performs within your application that involve their wallet (e.g., spending tokens, participating in in-game economies) are executed directly on their managed wallet. -4. **[User Withdrawal (Optional)](#transferring-tokens-from-managed-wallets):** If a user decides they want to take full control of their assets, you can facilitate the transfer of all funds from their managed wallet to their own self-custodial wallet. - -This process ensures that users can interact with blockchain assets seamlessly within your application without needing to understand the underlying complexities of blockchain technology or operating his own crypto wallet. - -:::info What you'll need: -- Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin Matrixchain to pay for . -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -- A [Collection](/02-guides/01-platform/01-managing-tokens/01-creating-collections.md) and a [Token](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) to add to the wallet. -::: - ---- - -## Create Managed Wallets - -To create a Managed wallet, run the `CreateWallet` mutation, with a unique ID as a parameter. - -Choose a unique `externalId` for each player/user that can be cross-referenced later. This unique identifier should be something already associated with the player in your database, such as a player ID or username. -By doing so, you will be able to consistently link the Managed Wallet to the respective player. - - - - -```graphql -mutation CreateManagedWallet { - CreateWallet(externalId: "player_1_id") #Replace this with a unique ID -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation CreateManagedWallet($external_id: String!) {\r\n CreateWallet(externalId: $external_id)\r\n}","variables":{"externalId":"player_1_id"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the mutation -var createWallet = new CreateWallet() - .SetExternalId("player_1_id"); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendCreateWallet(createWallet); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreMutations.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Set up the query - CreateWallet createWallet = CreateWallet() - .SetExternalId(make_shared("player_1_id")); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendCreateWallet(*client, createWallet); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& getCreateWalletResult = gqlResult->GetData()->GetResult(); - - std::cout << getCreateWalletResult.value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation CreateManagedWallet($external_id: String!) { - CreateWallet(externalId: $external_id) - } - `, - variables: { - externalId: "player_1_id" //Replace this with a unique ID - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation CreateManagedWallet($external_id: String!) { - CreateWallet(externalId: $external_id) - } - `, - variables: { - externalId: "player_1_id" //Replace this with a unique ID - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation CreateManagedWallet($external_id: String!) { - CreateWallet(externalId: $external_id) -} -''' - -variables = { - 'externalId': "player_1_id" #Replace this with a unique ID -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -:::warning Lost database data? -Recreate the Managed wallets by running `CreateWallet` mutation again for each of the `externalId`s. -Make sure to use the same Daemon wallet seed and password used to create Managed wallets prior, as Managed wallets are derived with the following derivation path: `walletSeed/externalId///password` -::: - -## Interact with Managed Wallets - -Once the Managed wallet is created, you can provide the `externalId` field of the Wallet object to any query or mutation that accepts a `Wallet.externalId` parameter, in order to use a Managed wallet instead of any other wallet. - -Lets look at the `GetWallet` query as an example, to [get the Public Key of an account](/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md), we use the account's address. `account: "address_here"` - -However, to get the Public Key of a `Managed wallet`, we provide the `Wallet.externalId` parameter instead. - -**Query:** - - - -```graphql -query GetManagedWalletPublicKey { - GetWallet(externalId: "player_1_id") { #Specify the managed wallet unique ID - account { - address - publicKey - } - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"query GetManagedWalletPublicKey($external_id: String!) {\r\n GetWallet(externalId: $external_id) {\r\n account {\r\n address\r\n publicKey\r\n }\r\n }\r\n}","variables":{"externalId":"player_1_id"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Setup the query -var getWallet = new GetWallet() - .SetExternalId("player_1_id"); - -// Define and assign the return data fragment to the query -var walletFragment = new WalletFragment() - .WithAccount(new AccountFragment() - .WithAddress() - .WithPublicKey() - ); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendGetWallet(getWallet); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreQueries.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Set up the query - GetWallet getWallet = GetWallet() - .SetExternalId(make_shared("player_1_id")); - - // Define and assign the return data fragments to the query - AccountFragment accountFragment = AccountFragment() - .WithAddress() - .WithPublicKey(); - WalletFragment walletFragment = WalletFragment() - .WithAccount(make_shared(accountFragment)); - - getWallet.SetFragment(make_shared(walletFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendGetWallet(*client, getWallet); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& getWalletResult = gqlResult->GetData()->GetResult(); - - std::cout << getWalletResult->GetAccount()->GetAddress().value() << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - query GetManagedWalletPublicKey($external_id: String!) { - GetWallet(externalId: $external_id) { - account { - address - publicKey - } - } - } - `, - variables: { - externalId: "player_1_id" //Specify the managed wallet unique ID - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - query GetManagedWalletPublicKey($external_id: String!) { - GetWallet(externalId: $external_id) { - account { - address - publicKey - } - } - } - `, - variables: { - externalId: "player_1_id" //Specify the managed wallet unique ID - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -query GetManagedWalletPublicKey($external_id: String!) { - GetWallet(externalId: $external_id) { - account { - address - publicKey - } - } -} -''' - -variables = { - 'externalId': "player_1_id" #Specify the managed wallet unique ID -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -**Response:** - -```json -{ - "data": { - "GetWallet": { - "account": { - "address": "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT", //Account address for Managed wallet ID "player_1_id" - "publicKey": "0x92f33d3efd6af37798b125cba08e21fc7b404293f12c067f1cb6ab326775ff08" - } - } - } -} -``` - -## Minting tokens to Managed wallets - -With the acquired Managed wallet address, you can mint tokens directly to the Managed wallet. - - - -```graphql -mutation BatchMint { - BatchMint( - collectionId: 7154 #Specify the collection ID - recipients: [ - { - account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" #The recipient of the mint (the Managed wallet account address from the GetWallet query) - mintParams: { - amount:1 #Amount to mint - tokenId: {integer: 6533} #Token ID to mint - } - } - ] - ) { - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation BatchMint($collection_id: BigInt!) {\r\n BatchMint(\r\n collectionId: $collection_id\r\n recipients: [\r\n {\r\n account: \"cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT\" #The recipient of the mint (the Managed wallet account address from the GetWallet query)\r\n mintParams: {\r\n amount: 1 #Amount to mint\r\n tokenId: { integer: 6533 } #Token ID to mint\r\n }\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":7154}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Create the list of recipients for the batch mint -var recipients = new List() -{ - new MintRecipient() - .SetAccount("cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT") - .SetMintParams(new MintTokenParams() - .SetAmount(1) - .SetTokenId(new EncodableTokenIdInput().SetInteger(6533) - ) - ) -}; - -// Setup the mutation -var batchMint = new BatchMint() - .SetCollectionId(7154) - .SetRecipients(recipients.ToArray()); - -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -batchMint.Fragment(transactionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendBatchMint(batchMint); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreMutations.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Set up the transaction parameters - EncodableTokenIdInput token1 = EncodableTokenIdInput() - .SetInteger(make_shared("6533")); - - MintTokenParams mintTokenParams = MintTokenParams() - .SetAmount(make_shared("1")) - .SetTokenId(make_shared(token1)); - - MintRecipient mintRecipient = MintRecipient() - .SetAccount(make_shared("cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT")) - .SetMintParams(make_shared(mintTokenParams)); - - vector recipients; - recipients.push_back(mintRecipient); - - // Set up the mutation - BatchMint batchMint = BatchMint() - .SetCollectionId(make_shared("7154")) - .SetRecipients(make_shared>(vector{mintRecipient})); - - TransactionFragment transactionFragment = TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - - batchMint.SetFragment(make_shared(transactionFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendBatchMint(*client, batchMint); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& batchMintResult = gqlResult->GetData()->GetResult(); - - std::cout << ToString(batchMintResult->GetMethod().value()) << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation BatchMint($collection_id: BigInt!) { - BatchMint( - collectionId: $collection_id - recipients: [ - { - account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" #The recipient of the mint (the Managed wallet account address from the GetWallet query) - mintParams: { - amount:1 #Amount to mint - tokenId: {integer: 6533} #Token ID to mint - } - } - ] - ) { - id - method - state - } - } - `, - variables: { - collection_id: 7154 //Specify the managed wallet unique ID - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation BatchMint($collection_id: BigInt!) { - BatchMint( - collectionId: $collection_id - recipients: [ - { - account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" #The recipient of the mint (the Managed wallet account address from the GetWallet query) - mintParams: { - amount:1 #Amount to mint - tokenId: {integer: 6533} #Token ID to mint - } - } - ] - ) { - id - method - state - } - } - `, - variables: { - collection_id: 7154 //Specify the managed wallet unique ID - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation BatchMint($collection_id: BigInt!) { - BatchMint( - collectionId: $collection_id - recipients: [ - { - account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" #The recipient of the mint (the Managed wallet account address from the GetWallet query) - mintParams: { - amount:1 #Amount to mint - tokenId: {integer: 6533} #Token ID to mint - } - } - ] - ) { - id - method - state - } -} -''' - -variables = { - 'collection_id': 7154 #Specify the managed wallet unique ID -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -## Transferring tokens from managed wallets - -If you followed along the previous snippets of code, you should have a Managed wallet with a token in it. To transfer it out to another wallet, we can use the `BatchTransfer` mutation. - -:::info Signing transactions using a managed wallet -It's important to note that every mutation that accepts a `signingAccount` argument can be used to sign the transaction using a managed wallet, as shown in the batch transfer tutorial below. -::: - - - -```graphql -mutation BatchTransfer { - BatchTransfer( - collectionId: 7154 #Specify the collection ID - signingAccount: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" # Add your signing account address (the Managed wallet account address from the GetWallet query) - recipients: [ - { - account: "cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ" #The recipient of the transfer - simpleParams: { - tokenId: { integer: 6533 } #Token ID to transfer - amount: 1 #Amount to transfer - } - } - ] - ) { - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation BatchTransfer($collection_id: BigInt!, $signing_account: String!) {\r\n BatchTransfer(\r\n collectionId: $collection_id\r\n signingAccount: $signing_account\r\n recipients: [\r\n {\r\n account: \"cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ\" #The recipient of the transfer\r\n simpleParams: {\r\n tokenId: { integer: 6533 } #Token ID to transfer\r\n amount: 1 #Amount to transfer\r\n }\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":7154,"signing_account":"cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT"}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Create the array of recipients for the batch transfer -var recipients = new List() -{ - new TransferRecipient() - .SetAccount("cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ") // The recipient of the transfer - .SetSimpleParams(new SimpleTransferParams() - .SetTokenId(new EncodableTokenIdInput().SetInteger(6533)) - .SetAmount(1) - ) -}; - -// Setup the mutation -var batchTransfer = new BatchTransfer() - .SetCollectionId(7154) - .SetSigningAccount("cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT") // Add your signing account address (the Managed wallet account address from the GetWallet query) - .SetRecipients(recipients.ToArray()); - -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -batchTransfer.Fragment(transactionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendBatchTransfer(batchTransfer); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreMutations.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -int main() { - - // Set up the transaction paramters - EncodableTokenIdInput token1 = EncodableTokenIdInput() - .SetInteger(make_shared("6533")); - - SimpleTransferParams simpleTransferParams = SimpleTransferParams() - .SetAmount(make_shared("1")) - .SetTokenId(make_shared(token1)); - - TransferRecipient transferRecipient = TransferRecipient() - .SetAccount(make_shared("cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ")) - .SetSimpleParams(make_shared(simpleTransferParams)); - - vector recipients; - recipients.push_back(transferRecipient); - - // Set up the mutation - BatchTransfer batchTransfer = BatchTransfer() - .SetCollectionId(make_shared("7154")) - .SetSigningAccount(make_shared("cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT")) - .SetRecipients(make_shared>(recipients)); - - TransactionFragment transactionFragment = TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - - batchTransfer.SetFragment(make_shared(transactionFragment)); - - // Create and auth a client to send the request to the platform - unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); - client->Auth("Your_Platform_Token_Here"); - - // Send the request then get the response and write the output to the console. - // Only the fields that were requested in the fragment will be filled in, - // other fields which weren't requested in the fragment will be set to null. - future>> futureResponse = SendBatchTransfer(*client, batchTransfer); - - // Get the platform response holding the HTTP data - PlatformResponsePtr> response = futureResponse.get(); - - // Get the result, a GraphQL response, holding the GraphQL data - const optional>& gqlResult = response->GetResult(); - - // Write the result data to the console - if (gqlResult.has_value() && gqlResult->IsSuccess()) - { - const optional& batchTransferResult = gqlResult->GetData()->GetResult(); - - std::cout << ToString(batchTransferResult->GetMethod().value()) << std::endl; - } - - // Write any error messages to the console - if (gqlResult.has_value() && gqlResult->HasErrors()) - { - const optional>& errors = gqlResult->GetErrors(); - - for (const GraphQlError& error : errors.value()) { - std::cout << error.GetMessage().value() << std::endl; - } - } - - client.reset(); - - return 0; -} -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation BatchTransfer - ( - $collection_id: BigInt! - $signing_account: String! - ) { - BatchTransfer( - collectionId: $collection_id - signingAccount: $signing_account - recipients: [ - { - account: "cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ" #The recipient of the transfer - simpleParams: { - tokenId: { integer: 6533 } #Token ID to transfer - amount: 1 #Amount to transfer - } - } - ] - ) { - id - method - state - } - } - `, - variables: { - collection_id: 7154, //Specify the collection ID - signing_account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" //Add your signing account address (the Managed wallet account address from the GetWallet query) - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation BatchTransfer - ( - $collection_id: BigInt! - $signing_account: String! - ) { - BatchTransfer( - collectionId: $collection_id - signingAccount: $signing_account - recipients: [ - { - account: "cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ" #The recipient of the transfer - simpleParams: { - tokenId: { integer: 6533 } #Token ID to transfer - amount: 1 #Amount to transfer - } - } - ] - ) { - id - method - state - } - } - `, - variables: { - collection_id: 7154, //Specify the collection ID - signing_account: "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" //Add your signing account address (the Managed wallet account address from the GetWallet query) - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation BatchTransfer -( - $collection_id: BigInt! - $signing_account: String! -) { - BatchTransfer( - collectionId: $collection_id - signingAccount: $signing_account - recipients: [ - { - account: "cxLf6yvvtscKrHRfKDphnzsT3eoRY45VbJvqXKub5pmj5mdbQ" #The recipient of the transfer - simpleParams: { - tokenId: { integer: 6533 } #Token ID to transfer - amount: 1 #Amount to transfer - } - } - ] - ) { - id - method - state - } -} -''' - -variables = { - 'collection_id': 7154, #Specify the collection ID - 'signing_account': "cxMkGKAmD73fGoFVaKj5HNmeLRHpTFDf5oQMp2dsqKJ8uZ3tT" #Add your signing account address (the Managed wallet account address from the GetWallet query) -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -Make sure that `signingAccount` is set to the Managed Wallet address that owns that token. - -:::info Explore More Arguments -For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -::: \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md deleted file mode 100644 index c63eae4..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md +++ /dev/null @@ -1,1057 +0,0 @@ ---- -title: "Using Fuel Tanks" -slug: "using-fuel-tanks" -description: "Discover how to use fuel tanks to cover blockchain transaction fees, creating a seamless and cost-free experience for users within your application." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -A Fuel tank is a pool that holds Enjin Coins (ENJ) which is used to cover transaction fees for eligible users. Each is designed to minimize transaction costs, resulting in lower expenses for platform operations. - -Fuel Tanks are flexible and dynamic, allowing customization based on specific rules and requirements. They operate based on a set of rules, making it possible to target specific actions or accounts meeting specific criteria. - -When dispatching a transaction via a Fuel Tank, the following checks must pass: - -- The Fuel Tank is not frozen. -- The Fuel Tank is funded with enough s. -- The dispatcher account is allowed to use the Fuel Tank. -- The Fuel Tank allows dispatching this transaction. - -:::info What you'll need: -- Some [Enjin Coin](/06-enjin-products/02-enjin-coin.md) on Enjin / Canary Matrixchain to pay for , and for funding the tank. -You can obtain cENJ (Canary ENJ) for testing from the [Canary faucet](https://faucet.canary.enjin.io/). -- An [Enjin Platform Account](/01-getting-started/04-using-the-enjin-platform.md). -::: - -## Tank Rules - -Fuel Tanks offer versatile rules for various use cases, allowing customization. - -### Dispatch Rules - -#### Rule Set - -When a call is made to a fuel tank (also known as "Dispatching"), it must be made in accordance with a set of rules. These rules, known as rule sets, can include multiple individual Dispatch Rules that determine the validity of the call. A fuel tank can have multiple rule sets, each of which controls access and permissions to the fuel tank's functionality and resources. - -#### Types of Dispatch Rules: - -- **`Whitelisted Callers`**: Subsidize s dispatched from certain s. -- **`Whitelisted Collections`**: Subsidize s involving specific . -- **`Require Token`**: Subsidize s only if the user holds a specific NFT. -- **`WhitelistedPallets`**: Subsidize s involving specific . -- **`Permitted Extrinsics`**: Subsidize s involving specific s, e.g. marketplace CreateListing transactions. -- **`Permitted Calls`**: Subsidize s involving specific s and parameters. -- **`User Fuel Budget`**: Set limits on individual fuel consumption for security. -- **`Tank Fuel Budget`**: Define collective fuel usage to extend Fuel Tank lifespan. -- **`Max Fuel Burn Per Transaction`**: Control fuel consumption per transaction for predictability. -- **`Require Signature`**: Subsidize s which were signed by a specific . - -These rules enhance the Enjin Blockchain experience, sparking new products and business models. - -#### RequireAccount Parameter - -In addition to Dispatch Rules, a Rule Set may require the dispatching account to have an existing Fuel Tank User Account in order to dispatch a call. This is configured by setting the Rule Set's `requireAccount` parameter to true. - -### Account Rules - -Adding an account to the Fuel Tank User Accounts can be done in two ways: - -1. In advance with the `add_account` extrinsic. -2. When dispatching a call with the `dispatch_and_touch` extrinsic. - -In both ways, the account is added to the tank's User Accounts only if it's successfully validated by the tank's Account Rules. - -Note: if the dispatcher is not the tank owner, he may add itself to the User Accounts only if [UserAccountManagement](#user-account-management) is configured - -#### Types of Account Rules: - -- **`Whitelisted Callers`**: Only listed accounts are able to add their account to the Fuel Tank's User Accounts. -- **`Require Token`**: Only accounts that hold the specified token are able to add their account to the Fuel Tank's User Accounts. - -### User Account Management - -By default, only the Fuel Tank owner has the permission to add accounts to the Fuel Tank User Accounts. However, the Fuel Tank can be configured to allow accounts to add themselves by setting the `tankReservesAccountCreationDeposit` argument. - -- **Default Behavior**: If the `tankReservesAccountCreationDeposit` argument is not provided, only the Fuel Tank owner can add accounts to the Fuel Tank User Accounts. -- **Self-Addition without Deposit Funding**: If the `tankReservesAccountCreationDeposit` argument is set to `false`, the Fuel Tank allows accounts to add themselves to the Fuel Tank User Accounts. However, the Fuel Tank will not provide the funds required for the Tank User Account . -- **Self-Addition with Deposit Funding**: If the `tankReservesAccountCreationDeposit` argument is set to `true`, the Fuel Tank allows accounts to add themselves to the Fuel Tank User Accounts, and it also covers the necessary funds required for the Tank User Account . - -### Coverage Policy - -By default, the fuel tank will subsidize only . -To cover both and any the dispatched call may require, set the Coverage Policy to `FEES_AND_DEPOSIT` - -## Creating Fuel Tanks - -Now that we have a basic understanding of how Fuel Tanks are structured, let's go through setting up some Fuel Tanks with specific functionality, including creating rules and customizing it to fit your needs, enabling you to reduce transaction costs for your users. - -:::info User Interface -This guide will demonstrate how to create various fuel tanks with the GraphQL API. -Note that you may also use the User Interface to create Fuel Tanks. - -To create a Fuel Tank using the Platform's User Interface, navigate to "**[Fuel Tanks](https://platform.canary.enjin.io/fuel-tanks)**" in the Platform Menu. Then, click the "**[Create Fuel Tank](https://platform.canary.enjin.io/create/fuel-tank)**" button. -::: - -:::warning **Feature Availability Notice** -Please be aware that the following arguments are currently unavailable on the Enjin Platform: -- `PermittedExtrinsics` cannot be set via the Enjin Platform UI. -- `WhitelistedPallets` is not supported on the Enjin Platform, both on the UI and API. -- `requireAccount` is also not supported on the Enjin Platform, both on the UI and API. -We will update the documentation once these options are available. -Code snippets with these arguments are provided for illustrative purposes only. If you wish to create a fuel tank with these options now, please use the Enjin Console at [console.enjin.io](https://console.enjin.io). -::: - -:::info Make sure to use the Fuel Tank Endpoint -- Testnet: `http://platform.canary.enjin.io/graphql/fuel-tanks` -- Mainnet: `http://platform.enjin.io/graphql/fuel-tanks` -**Try the Beam Playground here:** -- Testnet: http://platform.canary.enjin.io/graphiql/fuel-tanks -- Mainnet: http://platform.enjin.io/graphiql/fuel-tanks -::: - -### Subsidize Token Transfers For A Collection - -The following mutation will set up a fuel tank that only subsidizes transactions that contain a `BatchTransfer` extrinsic, and only if it involves a token from the collection with ID 36,105. - - - -```graphql -mutation CreateFuelTank{ - CreateFuelTank( - name: "Collection Token Transfers" #Specify the Fuel Tank name - coveragePolicy: FEES #This is set to FEES since we only want to subsidize transaction fees, and we don't need to provide storage deposits for token transfer transactions - reservesAccountCreationDeposit: null #This is set to null since we don't want to allow tank account creations - dispatchRules: [{ - permittedExtrinsics: [BatchTransfer] #This rule specifies that only batch transfers are subsidized - whitelistedCollections: [36105] #This rule ensures that only the specified collection is subsidized - }] - ){ - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation CreateFuelTank(\r\n $name: String!\r\n $coveragePolicy: CoveragePolicy\r\n $permittedExtrinsics: [TransactionMethod!]\r\n $whitelistedCollections: [BigInt!]\r\n) {\r\n CreateFuelTank(\r\n name: $name\r\n coveragePolicy: $coveragePolicy\r\n dispatchRules: [\r\n {\r\n permittedExtrinsics: $permittedExtrinsics #This rule specifies that only batch transfers are subsidized\r\n whitelistedCollections: $whitelistedCollections #This rule ensures that only the specified collection is subsidized\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"name":"Collection Token Transfers","coveragePolicy":"FEES","permittedExtrinsics":["BatchTransfer"],"whitelistedCollections":[36105]}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; -using Enjin.Platform.Sdk.FuelTanks; - -// Create the array of dispatch rules for the fuel tank -var dispatchRules = new List() -{ - new DispatchRuleInputType() - .SetPermittedExtrinsics(TransactionMethod.BatchTransfer) // This rule specifies that only batch transfers are subsidized - .SetWhitelistedCollections(36105) // This rule ensures that only the specified collection is subsidized -}; - -// Setup the mutation -var createFuelTank = new CreateFuelTank() - .SetName("Collection Token Transfers") // Set the name of the fuel tank - .SetCoveragePolicy(CoveragePolicy.Fees) // This is set to FEES since we only want to subsidize transaction fees, and we don't need to provide storage deposits for token transfer transactions - .SetReservesAccountCreationDeposit(null) // This is set to null since we don't want to allow tank account creations - .SetDispatchRules(dispatchRules.ToArray()); - -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -createFuelTank.Fragment(transactionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendCreateFuelTank(createFuelTank); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -// Coming Soon! -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation CreateFuelTank - ( - $name: String! - $coveragePolicy: CoveragePolicy - $permittedExtrinsics: [TransactionMethod!] - $whitelistedCollections: [BigInt!] - ) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - dispatchRules: [{ - permittedExtrinsics: $permittedExtrinsics #This rule specifies that only batch transfers are subsidized - whitelistedCollections: $whitelistedCollections #This rule ensures that only the specified collection is subsidized - }] - ){ - id - method - state - } - } - `, - variables: { - name: "Collection Token Transfers", //Specify the Fuel Tank name - coveragePolicy: "FEES", //This is set to FEES since we only want to subsidize transaction fees, and we don't need to provide storage deposits for token transfer transactions - permittedExtrinsics: ["BatchTransfer"], //This rule specifies that only batch transfers are subsidized - whitelistedCollections: [36105] //This rule ensures that only the specified collection is subsidized - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation CreateFuelTank - ( - $name: String! - $coveragePolicy: CoveragePolicy - $permittedExtrinsics: [TransactionMethod!] - $whitelistedCollections: [BigInt!] - ) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - dispatchRules: [{ - permittedExtrinsics: $permittedExtrinsics #This rule specifies that only batch transfers are subsidized - whitelistedCollections: $whitelistedCollections #This rule ensures that only the specified collection is subsidized - }] - ){ - id - method - state - } - } - `, - variables: { - name: "Collection Token Transfers", //Specify the Fuel Tank name - coveragePolicy: "FEES", //This is set to FEES since we only want to subsidize transaction fees, and we don't need to provide storage deposits for token transfer transactions - permittedExtrinsics: ["BatchTransfer"], //This rule specifies that only batch transfers are subsidized - whitelistedCollections: [36105] //This rule ensures that only the specified collection is subsidized - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation CreateFuelTank - ( - $name: String! - $coveragePolicy: CoveragePolicy - $permittedExtrinsics: [TransactionMethod!] - $whitelistedCollections: [BigInt!] - ) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - dispatchRules: [{ - permittedExtrinsics: $permittedExtrinsics #This rule specifies that only batch transfers are subsidized - whitelistedCollections: $whitelistedCollections #This rule ensures that only the specified collection is subsidized - }] - ){ - id - method - state - } - } -''' - -variables = { - 'name': "Collection Token Transfers", #Specify the Fuel Tank name - 'coveragePolicy': "FEES", #This is set to FEES since we only want to subsidize transaction fees, and we don't need to provide storage deposits for token transfer transactions - 'permittedExtrinsics': ["BatchTransfer"], #This rule specifies that only batch transfers are subsidized - 'whitelistedCollections': [36105] #This rule ensures that only the specified collection is subsidized -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -### Subsidize Any Transaction Involving s From A Certain Collection - -:::warning Upcoming Feature Notice: `WhitelistedPallets` Option -The `WhitelistedPallets` argument is not yet supported on the Enjin Platform. -The following code snippet is provided for illustrative purposes only. -If you wish to create a fuel tank with this option now, please use the Enjin Console at [console.enjin.io](https://console.enjin.io). -We will update the documentation once this option is available. -::: - -The following mutation will set up a fuel tank that only subsidizes transactions that involves any token from the collection with ID 36,105. To achieve this, we are using the same dispatch rule from the previous example (`whitelistedCollections`) and the `WhitelistedPallets` rule, allowing only extrinsics from the `MultiTokens` pallet (for actions such as `send`, `mint`, `transfer`, etc.) and `Marketplace` pallet (for actions such as `list`, `buy`, `offer`, etc. ). - - - -```graphql -mutation CreateFuelTank{ - CreateFuelTank( - name: "Collection Token Actions" #Specify the Fuel Tank name - coveragePolicy: FEES_AND_DEPOSIT #This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). - reservesAccountCreationDeposit: true #This is set to true since we want to allow tank account creations, and we want the tank to subsidize the ENJ required for the required for the Tank User Account storage deposit - dispatchRules: [{ - WhitelistedPallets: [MultiTokens, Marketplace] #This rule specifies that only extrinsics from the MultiTokens and Marketplace pallets are subsidized - whitelistedCollections: [36105] #This rule ensures that only the specified collection is subsidized - }] - ){ - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation CreateFuelTank(\r\n $name: String!\r\n $coveragePolicy: CoveragePolicy\r\n $reserveAccountCreationDeposit: Boolean\r\n $whitelistedPallets: [String!]\r\n $whitelistedCollections: [BigInt!]\r\n) {\r\n CreateFuelTank(\r\n name: $name\r\n coveragePolicy: $coveragePolicy\r\n reservesAccountCreationDeposit: $reserveAccountCreationDeposit\r\n dispatchRules: [\r\n {\r\n whitelistedPallets: $whitelistedPallets\r\n whitelistedCollections: $whitelistedCollections\r\n }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"name":"Collection Token Actions","coveragePolicy":"FEES_AND_DEPOSIT","reserveAccountCreationDeposit":true,"whitelistedPallets":["MultiTokens","Marketplace"],"whitelistedCollections":[36105]}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; -using Enjin.Platform.Sdk.FuelTanks; - -// Create the array of dispatch rules for the fuel tank -var dispatchRules = new List() -{ - new DispatchRuleInputType() - .SetWhitelistedPallets(Pallets.MultiTokens, Pallets.Marketplace) // Note: This is not yet implemented in the Platform API. This rule specifies that only extrinsics from the MultiTokens and Marketplace pallets are subsidized. - .SetWhitelistedCollections(36105) // This rule ensures that only the specified collection is subsidized. -}; - -// Setup the mutation -var createFuelTank = new CreateFuelTank() - .SetName("Collection Token Actions") // Set the name of the fuel tank - .SetCoveragePolicy(CoveragePolicy.FeesAndDeposit) // This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). - .SetReservesAccountCreationDeposit(true) // This is set to true since we want to allow tank account creations, and we want the tank to subsidize the ENJ required for the required for the Tank User Account storage deposit. - .SetDispatchRules(dispatchRules.ToArray()); - -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -createFuelTank.Fragment(transactionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendCreateFuelTank(createFuelTank); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -// Coming Soon! -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation CreateFuelTank( - $name: String! - $coveragePolicy: CoveragePolicy - $reserveAccountCreationDeposit: Boolean - $whitelistedPallets: [String!] - $whitelistedCollections: [BigInt!] - ) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - reservesAccountCreationDeposit: $reserveAccountCreationDeposit - dispatchRules: [ - { - whitelistedPallets: $whitelistedPallets - whitelistedCollections: $whitelistedCollections - } - ] - ) { - id - method - state - } - } - `, - variables: { - name: "Collection Token Actions", //Specify the Fuel Tank name - coveragePolicy: "FEES_AND_DEPOSIT", //This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). - reserveAccountCreationDeposit: true, //This is set to true since we want to allow tank account creations, and we want the tank to subsidize the ENJ required for therequired for the Tank User Account storage deposit - whitelistedPallets: ["MultiTokens", "Marketplace"], //This rule specifies that only extrinsics from the MultiTokens and Marketplace pallets are subsidized - whitelistedCollections: [36105] //This rule ensures that only the specified collection is subsidized - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation CreateFuelTank( - $name: String! - $coveragePolicy: CoveragePolicy - $reserveAccountCreationDeposit: Boolean - $whitelistedPallets: [String!] - $whitelistedCollections: [BigInt!] - ) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - reservesAccountCreationDeposit: $reserveAccountCreationDeposit - dispatchRules: [ - { - whitelistedPallets: $whitelistedPallets - whitelistedCollections: $whitelistedCollections - } - ] - ) { - id - method - state - } - } - `, - variables: { - name: "Collection Token Actions", //Specify the Fuel Tank name - coveragePolicy: "FEES_AND_DEPOSIT", //This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). - reserveAccountCreationDeposit: true, //This is set to true since we want to allow tank account creations, and we want the tank to subsidize the ENJ required for therequired for the Tank User Account storage deposit - whitelistedPallets: ["MultiTokens", "Marketplace"], //This rule specifies that only extrinsics from the MultiTokens and Marketplace pallets are subsidized - whitelistedCollections: [36105] //This rule ensures that only the specified collection is subsidized - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation CreateFuelTank( - $name: String! - $coveragePolicy: CoveragePolicy - $reserveAccountCreationDeposit: Boolean - $whitelistedPallets: [String!] - $whitelistedCollections: [BigInt!] -) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - reservesAccountCreationDeposit: $reserveAccountCreationDeposit - dispatchRules: [ - { - whitelistedPallets: $whitelistedPallets - whitelistedCollections: $whitelistedCollections - } - ] - ) { - id - method - state - } -} -''' - -variables = { - 'name': "Collection Token Actions", #Specify the Fuel Tank name - 'coveragePolicy': "FEES_AND_DEPOSIT", #This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). - 'reserveAccountCreationDeposit': True, #This is set to true since we want to allow tank account creations, and we want the tank to subsidize the ENJ required for therequired for the Tank User Account storage deposit - 'whitelistedPallets': ["MultiTokens", "Marketplace"], #This rule specifies that only extrinsics from the MultiTokens and Marketplace pallets are subsidized - 'whitelistedCollections': [36105] #This rule ensures that only the specified collection is subsidized -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -### Subsidize All Transactions For Whitelisted Accounts With Budget Limitations - -The following mutation will set up a fuel tank that subsidizes any transaction, the tank is allowed to subsidize up to 5 ENJ per 30 days for each User Account in the tank, which can be created only if the account is within the whitelisted callers list. - -:::warning Upcoming Feature Notice: `requireAccount` Option -The `requireAccount` argument is not yet supported on the Enjin Platform. -The following code snippet is provided for illustrative purposes only. -If you wish to create a fuel tank with this option now, please use the Enjin Console at [console.enjin.io](https://console.enjin.io). -We will update the documentation once this option is available. -::: - - - -```graphql -mutation CreateFuelTank{ - CreateFuelTank( - name: "Only Specific Accounts Allowed" #Specify the Fuel Tank name - coveragePolicy: FEES_AND_DEPOSIT #This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it. - accountRules: { - whitelistedCallers: [ #This will validate that the caller is whitelisted at the time of adding the caller's account to the tank's User Accounts. - "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", - "cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP" - ] - } - dispatchRules: [{ - requireAccount: true #This is set to true since we want the tank to allow dispatching only if the caller has a User Account in the tank. - userFuelBudget: { #In here we configure how much ENJ to subsidize for each User Account. - amount: 5000000000000000000 #This will allow the tank to subsidize up to 5 ENJ from the Tank's pool per period for each User Account. - resetPeriod: 432000 #This sets the period to 432,000 blocks, which is 30 days on average with 6 seconds average block time. Meaning the tank will be able to subsidize up to 5 ENJ per 30 days for each User Account. - } - }] - ){ - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation CreateFuelTank(\r\n $name: String!\r\n $coveragePolicy: CoveragePolicy\r\n $whitelistedCallers: [String!]\r\n $requireAccount: Boolean\r\n $userFuelBudget: FuelBudgetInputType\r\n) {\r\n CreateFuelTank(\r\n name: $name\r\n coveragePolicy: $coveragePolicy\r\n accountRules: { whitelistedCallers: $whitelistedCallers }\r\n dispatchRules: [\r\n { requireAccount: $requireAccount, userFuelBudget: $userFuelBudget }\r\n ]\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"name":"Only Specific Accounts Allowed","coveragePolicy":"FEES","whitelistedCallers":["cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu","cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP"],"requireAccount":true,"userFuelBudget":{"amount":5000000000000000000,"resetPeriod":432000}}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; -using Enjin.Platform.Sdk.FuelTanks; - -// Create an array of whitelisted callers and assign to accountRules.whitelistedCallers -var whitelistedCallers = new List() -{ - "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", - "cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP" -}; -var accountRules = new AccountRuleInputType() - .SetWhitelistedCallers(whitelistedCallers.ToArray()); - -// Create the user fuel budget input type -var userFuelBudget = new FuelBudgetInputType() - .SetAmount(5000000000000000000) // Set the amount of ENJ that the fuel tank will provide to users. - .SetResetPeriod(432000); // Set the reset period to 432000 blocks (approximately 30 days). - -// Create the array of dispatch rules for the fuel tank -var dispatchRules = new List() -{ - new DispatchRuleInputType() - .SetRequireAccount(true) // Note: This property has yet to be implemented on the platform. This is set to true since we want the tank to allow dispatching only if the caller has a User Account in the tank. - .SetUserFuelBudget(userFuelBudget) // Set the user fuel budget for the dispatch rule. -}; - -// Set up the mutation -var createFuelTank = new CreateFuelTank() - .SetName("Only Specific Accounts Allowed") // Set the name of the fuel tank. - .SetCoveragePolicy(CoveragePolicy.FeesAndDeposit) // This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it (such as listing a token for sale). - .SetAccountRules(accountRules) // Set the account rules for the fuel tank. - .SetDispatchRules(dispatchRules.ToArray()); - -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -createFuelTank.Fragment(transactionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendCreateFuelTank(createFuelTank); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -// Coming Soon! -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation CreateFuelTank( - $name: String! - $coveragePolicy: CoveragePolicy - $whitelistedCallers: [String!] - $requireAccount: Boolean - $userFuelBudget: FuelBudgetInputType - ) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - accountRules: { whitelistedCallers: $whitelistedCallers } - dispatchRules: [ - { requireAccount: $requireAccount, userFuelBudget: $userFuelBudget } - ] - ) { - id - method - state - } - } - `, - variables: { - name: "Only Specific Accounts Allowed", //Specify the Fuel Tank name - coveragePolicy: "FEES", //This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it. - whitelistedCallers: [ //This will validate that the caller is whitelisted at the time of adding the caller's account to the tank's User Accounts. - "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", - "cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP" - ], - requireAccount: true, //This is set to true since we want the tank to allow dispatching only if the caller has a User Account in the tank. - userFuelBudget: { //In here we configure how much ENJ to subsidize for each User Account. - amount: 5000000000000000000, //This will allow the tank to subsidize up to 5 ENJ from the Tank's pool per period for each User Account. - resetPeriod: 432000 //This sets the period to 432,000 blocks, which is 30 days on average with 6 seconds average block time. Meaning the tank will be able to subsidize up to 5 ENJ per 30 days for each User Account. - } - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation CreateFuelTank( - $name: String! - $coveragePolicy: CoveragePolicy - $whitelistedCallers: [String!] - $requireAccount: Boolean - $userFuelBudget: FuelBudgetInputType - ) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - accountRules: { whitelistedCallers: $whitelistedCallers } - dispatchRules: [ - { requireAccount: $requireAccount, userFuelBudget: $userFuelBudget } - ] - ) { - id - method - state - } - } - `, - variables: { - name: "Only Specific Accounts Allowed", //Specify the Fuel Tank name - coveragePolicy: "FEES", //This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it. - whitelistedCallers: [ //This will validate that the caller is whitelisted at the time of adding the caller's account to the tank's User Accounts. - "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", - "cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP" - ], - requireAccount: true, //This is set to true since we want the tank to allow dispatching only if the caller has a User Account in the tank. - userFuelBudget: { //In here we configure how much ENJ to subsidize for each User Account. - amount: 5000000000000000000, //This will allow the tank to subsidize up to 5 ENJ from the Tank's pool per period for each User Account. - resetPeriod: 432000 //This sets the period to 432,000 blocks, which is 30 days on average with 6 seconds average block time. Meaning the tank will be able to subsidize up to 5 ENJ per 30 days for each User Account. - } - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation CreateFuelTank( - $name: String! - $coveragePolicy: CoveragePolicy - $whitelistedCallers: [String!] - $requireAccount: Boolean - $userFuelBudget: FuelBudgetInputType -) { - CreateFuelTank( - name: $name - coveragePolicy: $coveragePolicy - accountRules: { whitelistedCallers: $whitelistedCallers } - dispatchRules: [ - { requireAccount: $requireAccount, userFuelBudget: $userFuelBudget } - ] - ) { - id - method - state - } -} -''' - -variables = { - 'name': "Only Specific Accounts Allowed", #Specify the Fuel Tank name - 'coveragePolicy': "FEES", #This is set to FEES_AND_DEPOSIT since we want the tank to provide ENJ for both transaction fees, and storage deposits for transactions that require it. - 'whitelistedCallers': [ #This will validate that the caller is whitelisted at the time of adding the caller's account to the tank's User Accounts. - "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", - "cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP" - ], - 'requireAccount': True, #This is set to true since we want the tank to allow dispatching only if the caller has a User Account in the tank. - 'userFuelBudget': { #In here we configure how much ENJ to subsidize for each User Account. - 'amount': 5000000000000000000, #This will allow the tank to subsidize up to 5 ENJ from the Tank's pool per period for each User Account. - 'resetPeriod': 432000 #This sets the period to 432,000 blocks, which is 30 days on average with 6 seconds average block time. Meaning the tank will be able to subsidize up to 5 ENJ per 30 days for each User Account. - } -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -A WebSocket event will also be fired so you can pick up the Fuel Tank creation in real time by listening to the app channel on the WebSocket. - -:::info Explore More Arguments -For a comprehensive view of all available arguments for queries and mutations, please refer to our [API Reference](/03-api-reference/03-api-reference.md). This resource will guide you on how to use the GraphiQL Playground to explore the full structure and functionality of our API. -::: - -## Dispatching a Call Using a Fuel Tank - -To broadcast a transaction call using a fuel tank, use the `Dispatch`/`DispatchAndTouch` mutation. - -:::tip The Fuel Tank requires a UserAccount to dispatch? -Use the `DispatchAndTouch` mutation to create a `UserAccount` and `Dispatch` at the same time in a single transaction. -::: - -:::tip Not sure which Fuel Tank to select? -If you need help figuring out the best fuel tank to use for a transaction, check out this page: [Selecting a fuel tank to dispatch with](/04-enjin-blockchain/03-enjin-matrixchain/02-fuel-tank-pallet.md#selecting-a-fuel-tank-to-dispatch-with). -::: - -### Step #1: Prepare The Mutation - -First, prepare the mutation you wish to dispatch, with the `id` and `encodedData` fields in the response. -In this example, we'll dispatch a call to send a from one account to another: - -:::danger Ask for the `id` and `encodedData` fields! -Make sure to add the `id` and `encodedData` fields in the response, or the mutation will fail with an error. -::: - -```graphql -mutation TransferNFT{ - SimpleTransferToken( - collectionId: 3298 - recipient: "efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr" - params: {tokenId: {integer: 1} amount: 1} - ){ - id - encodedData - } -} -``` - -### Step #2: Prepare The Dispatch Mutation - -Next, you need to convert the mutation call into a String. Remove all new lines and escape double quotation marks: - -``` -mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}} -``` - -To escape quotation marks you can use online tools such as https://tools.knowledgewalls.com/online-escape-single-or-double-quotes-from-string. - -### Step #3: Send the Dispatch Call - -Insert the mutation String from Step #2 into a Dispatch mutation call "query" parameter and send it: - - - -```graphql -mutation Dispatch { - Dispatch( - tankId: "efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou" #Specify the Fuel Tank ID to dispatch with - ruleSetId: 0 #Specify the ruleset to dispatch with - dispatch: { - call: MULTI_TOKENS #Specify the pallet to use for the transaction - query: "mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}" #Insert the mutation to dispatch. - variables: null #If the mutation has variables, insert them here. - } - ){ - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation Dispatch(\r\n $tankId: String!\r\n $ruleSetId: BigInt!\r\n $call: DispatchCall!\r\n $query: String!\r\n $variables: Object\r\n) {\r\n Dispatch(\r\n tankId: $tankId\r\n ruleSetId: $ruleSetId\r\n dispatch: { call: $call, query: $query, variables: $variables }\r\n ) {\r\n id\r\n method\r\n state\r\n }\r\n}\r\n","variables":{"tankId":"efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou","ruleSetId":0,"call":"MULTI_TOKENS","query":"mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}","variables":null}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; -using Enjin.Platform.Sdk.FuelTanks; - -// Create an array of whitelisted callers and assign to accountRules.whitelistedCallers -var dispatchInput = new DispatchInputType() - .SetCall(DispatchCall.MultiTokens) - .SetQuery("mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \\\"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\\\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}") // Insert the mutation to dispatch. - .SetVariables(null); - -// Set up the mutation -var dispatch = new Dispatch() - .SetTankId("efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou") - .SetRuleSetId(0) - .SetDispatch(dispatchInput); - -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -dispatch.Fragment(transactionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendDispatch(dispatch); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -// Coming Soon! -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation Dispatch( - $tankId: String! - $ruleSetId: BigInt! - $call: DispatchCall! - $query: String! - $variables: Object - ) { - Dispatch( - tankId: $tankId - ruleSetId: $ruleSetId - dispatch: { call: $call, query: $query, variables: $variables } - ) { - id - method - state - } - } - `, - variables: { - tankId: "efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou", //Specify the Fuel Tank ID to dispatch with - ruleSetId: 0, //Specify the ruleset to dispatch with - call: "MULTI_TOKENS", //Specify the pallet to use for the transaction - query: "mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}", //Insert the mutation to dispatch. - variables: null //If the mutation has variables, insert them here as an object. - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation Dispatch( - $tankId: String! - $ruleSetId: BigInt! - $call: DispatchCall! - $query: String! - $variables: Object - ) { - Dispatch( - tankId: $tankId - ruleSetId: $ruleSetId - dispatch: { call: $call, query: $query, variables: $variables } - ) { - id - method - state - } - } - `, - variables: { - tankId: "efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou", //Specify the Fuel Tank ID to dispatch with - ruleSetId: 0, //Specify the ruleset to dispatch with - call: "MULTI_TOKENS", //Specify the pallet to use for the transaction - query: "mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}", //Insert the mutation to dispatch. - variables: null //If the mutation has variables, insert them here as an object. - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation Dispatch( - $tankId: String! - $ruleSetId: BigInt! - $call: DispatchCall! - $query: String! - $variables: Object -) { - Dispatch( - tankId: $tankId - ruleSetId: $ruleSetId - dispatch: { call: $call, query: $query, variables: $variables } - ) { - id - method - state - } -} -''' - -variables = { - 'tankId': "efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou", #Specify the Fuel Tank ID to dispatch with - 'ruleSetId': 0, #Specify the ruleset to dispatch with - 'call': "MULTI_TOKENS", #Specify the pallet to use for the transaction - 'query': "mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}", #Insert the mutation to dispatch. - 'variables': None #If the mutation has variables, insert them here as an object. -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -Once the mutation is sent, signed and broadcasted, If the transaction is eligible, the fuel tank will subsidize the transaction fees; otherwise, the transaction will fail. - -:::info Need to broadcast from Managed Wallet? -To broadcast the transaction from a managed wallet account, add the `signingAccount` argument as instructed in the [Using Managed Wallets](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md#transferring-tokens-from-managed-wallets) page. -It's important to note that the `signingAccount` argument should be added on the `Dispatch` mutation level, and **NOT** in the dispatch argument. -In the above example, to transfer the token from a managed wallet via a fuel tank: - -```graphql -mutation Dispatch { - Dispatch( - tankId: "efQqqMFeDXMSQ43rShznQQ5Aq5pnMUKBfvTQHntatMmF4JZou" #Specify the Fuel Tank ID to dispatch with - ruleSetId: 0 #Specify the ruleset to dispatch with - signingAccount: "Managed Wallet Address Here" - dispatch: { - call: MULTI_TOKENS #Specify the pallet to use for the transaction - query: "mutation TransferNFT{SimpleTransferToken(collectionId: 3298 recipient: \"efQh8FzLm6oH3dmTU3HWqGrtm6Xcuu1WG33N2Ka9fzo5MFFAr\" params: {tokenId: {integer: 1} amount: 1}){id encodedData}}" #Insert the mutation to dispatch. - variables: null #If the mutation has variables, insert them here. - } - ){ - id - method - state - } -} -``` -::: \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/_category_.json deleted file mode 100644 index e434e3c..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/02-managing-users/_category_.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "label": "Managing Users" -} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md deleted file mode 100644 index 1b01b7d..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -title: "TokenID Structure" -slug: "tokenid-structure" -description: "Learn about the Enjin TokenID structure, its format, and how to utilize it effectively to create and manage blockchain-based assets in your projects." ---- -# Token ID Structure - -Token IDs in a collection serve as unique identifiers for tokens, represented as 128-bit integers. Beyond their primary role, token IDs can also store structured information about a token's attributes, providing greater organizational flexibility. - -This guide outlines recommended approaches for structuring and organizing token IDs, as well as the four encoding options available on the Enjin Platform: **ERC1155**, **Hash**, **StringId**, and **Integer** (no encoding). - -*** - -## Token ID Structuring Approaches - -There are 4 key methods for Token ID organization: - -1. [Bitmasks](#1-bitmasks) -2. [Ranges](#2-ranges) -3. [Hashes](#3-hashes) -4. [Integer (No Encoding)](#4-integer-no-encoding) - -*** - -### 1. **Bitmasks** - -Bitmasks allocate specific bits of the 128-bit token ID to various attributes, such as **game**, **server**, **class**, and **item ID**. Each section of the token ID is defined by the bit size required for that attribute, ensuring efficient use of space. - -For example: - -- **Game ID**: 4 bits (up to 16 games) -- **Server ID**: 8 bits (up to 256 servers) -- **Class ID**: 16 bits (up to 65,536 classes) -- **Item ID**: Remaining 100 bits - -If you create a token with the ID `0x05ffa34f000000000000000000000001`, it can be decoded as: - -- **Game ID**: `05` (Game 5) -- **Server ID**: `ff` (Server 255) -- **Class ID**: `a34f` (Sword) -- **Item ID**: `1` - -This method is compact but requires encoding and decoding logic. - -*** - -### 2. **Ranges** - -Ranges define numeric intervals for each category, making the structure simpler to understand and decode. - -for example: - -- **Game 01**: Token IDs 0–1000 - - **Server 01**: 0–300 - - **Class A**: 0–20 - - **Class B**: 20–40 - - **Server 02**: 300–600 - -Token ID `325` maps to: - -- **Game ID**: `01` -- **Server ID**: `02` -- **Class ID**: `B` -- **Item ID**: `5` - -This approach is intuitive but may require manual configuration of ID ranges. - -*** - -### 3. **Hashes** - -Hashes use cryptographic functions like **SHA256**, **Keccak256**, or **Blake2** to generate unique token IDs. The input can include attributes such as game name, server, or class. - -for example: - -``` -{ - token_id: hash(hash("Game01") || hash("Server01") || hash("Sword")) -} -``` - -This guarantees uniqueness but makes decoding impossible, as hashing is a one-way process. - -*** - -### 4. **Integer (No Encoding)** - -Using Integer IDs involves assigning unique and raw integers without additional encoding. This is the simplest method but does not include embedded metadata. - -for example: - -Token IDs are manually assigned, such as `1`, `2`, `3`, etc. This method is straightforward but lacks flexibility for organization. - -*** - -## Enjin Platform Token ID Encoders - -The Enjin Platform provides built-in encoders to convert data into 128-bit token IDs: - -### 1. **ERC1155 Encoder** - -This encoder uses a 16-bit hexadecimal token ID and a 64-bit integer index. - -```graphql -tokenId: { - erc1155: { - tokenId: "0x78c0000000003377", index: 10 - } -} -``` - -Resulting Token ID: `160504280491028834688987873652194148362` - -*** - -### 2. **Hash Encoder** - -This encoder generates a token ID from a hashed array or object. - -for example: - -```graphql -tokenId: { - hash: { - Token: "Sword", Number: 5 - } -} -``` - -Resulting Token ID: `65304702016350863193892557847492631289` - -Token IDs generated with this method cannot be reversed or decoded. - -:::info Irreversible Token IDs with the Hash Encoder -Token IDs generated using the Hash Encoder are derived from a cryptographic hash function. As hashing is a one-way process, these token IDs cannot be reversed or decoded back into the original data. -::: - -:::danger Important Note -The Hash Encoder generates token IDs by hashing input data. If the resulting hash exceeds 128 bits, the encoder will fail. -::: - -*** - -### 3. **StringId Encoder** - -This encoder converts a string into a numeric token ID. - -for example: - -```graphql -tokenId: { - stringId: "MyToken-0001" -} -``` - -Resulting Token ID: `23977024514528806328972881969` - -:::danger Important Note -The StringId Encoder converts strings into numeric token IDs. Strings that produce integers larger than 128 bits will cause the encoding to fail. -::: - -*** - -### 4. **Integer** - -For direct assignment, integers can be passed as-is to create token IDs. - -for example: - -```graphql -tokenId: {integer: 1001} -``` - -Resulting Token ID: `1001` - -This approach is simple and requires no encoding or decoding. - -*** - -## Choosing the Right Approach - -### Considerations: - -- **Ease of Use**: Use Integers or ranges for simplicity. -- **Scalability**: Bitmasks and hashes work best for large collections with many attributes. -- **Interoperability**: Use the ERC1155 encoder for compatibility with Ethereum-based assets. - -By understanding these methods and tools, you can design token ID structures that best suit your project's needs while ensuring flexibility and scalability. diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-example.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-example.md deleted file mode 100644 index 7f334f0..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-example.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -title: "Metadata Example" -slug: "metadata-example" -description: "View detailed examples of how to implement metadata within your blockchain tokens, following Enjin’s guidelines for token structure and data representation." ---- -In this page we will create JSON formatted metadata for a collection and a token, following the [Metadata Standard](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md). - -You may set the collection/token `uri` on-chain attribute to a resource containing the collection's/token's metadata in JSON format, or set each individual metadata with an on-chain attribute, using the metadata type as the attribute key. -For example, to set the token's description individually, set the token's on-chain attribute key to `description`, and provide the description as the attribute's value. -If both on-chain and off-chain attributes are set, the on-chain attributes will take precedence over the off-chain attributes. - -:::warning **Important:** Attribute keys are case sensitive. -Ensure you use the correct casing when defining attributes to avoid errors. -::: - -## Collection Metadata - -```json -{ - "name": "The Multiverse", - "description": "A blockchain gaming multiverse is a collective gaming reality created by integrating a collection of blockchain assets with multiple games. In other words, a gaming multiverse is a collaborative gaming project where multiple game developers agree to use the same shared, decentralized database for some (or even all) of their in-game assets.\nThis enables gamers to utilize a multiverse asset in every game that is a part of a specific gaming multiverse (e.g., if a player owns a sword in Game A, they will also own it and can use it in Game B). In-game assets in a blockchain gaming multiverse are owned by gamers, while individual game developers control only the games they create.\nThe Enjin Multiverse is the first blockchain gaming multiverse ever.\nIt was announced on August 26, 2018, when six games (9Lives Arena, Age of Rust, Bitcoin Hodler, CryptoFights, Forest Knight, and War of Crypta) decided to collaborate and implement the first shared blockchain assets.\nIt has since grown to over 30 games.", - "media": [ - { - "url": "https://cdn.enjinx.io/assets/images/ethereum/platform/0/apps/8/2b728df41fadef568e4410fb823999d14473ef1e.jpeg", - "type": "image/jpeg" - }, - { - "url": "https://cdn.enjin.io/mint/image/15.jpg", - "type": "image/jpg" - }, - - ], - "banner_image": "https://platform.production.enjinusercontent.com/enterprise/enjin/assets/media/2024-multiverse.banner.jpg", - "fallback_image": "https://cdn.enjin.io/mint/image/15.jpg", - "external_url": "https://enjin.io/multiverse" -} -``` - -### Attribute Keys Explained: - -- `name`: The collection's display name. -- `description`: The collection's description. -- `media`: A list of links to media files (images, GIFs, videos, etc.) to be displayed as the collection media. - - The NFT.io marketplace uses the first provided media file as the collection's featured media. -- `banner_image`: A link to an image file to be displayed as the collection banner. (Recommended ratio: 4:1) -- `fallback_image`: A link to an alternative image file to be used when the media provided in the `media` list fails to load. -- `external_url`: A link to the collection's website. - -## Token Metadata - -```json -{ - "name": "Primythical Chest", - "description": "As written in the fourth edition of 'Book of the Ruindawn', Primythical Chests contain fabled treasures, and are hidden through a myriad of universes by the Creators of Realms themselves—the Architects.\nThe wood-and-iron vaults are shrouded by the veils of time and space—and can only be found by the most courageous and intelligent Wanderers.", - "media": [ - { - "url": "https://cdn.enjin.io/mint/images/50000000000000c3.jpg", - "type": "image/jpg" - }, - { - "url": "https://cdn.enjin.io/mint/videos/chest.mp4", - "type": "video/mp4", - }, - { - "url": "https://cdn.enjin.io/mint/models/chest.glb", - "type": "model/gltf-binary", - } - ], - "fallback_image": "https://cdn.enjin.io/mint/images/50000000000000c3.jpg", - "attributes": { - "Usable In": { - "value": "Lost Relics, Etherscape, Axiomech" - }, - "Artifact Rarity": { - "value": "Common" - } - }, - "external_url": "https://enjin.io/multiverse", - "keywords": [ - "Multiverse Item", - "Key", - "Free" - ] -} -``` - -### Attribute Keys Explained: - -- `name`: The token's display name. -- `description`: The token's description. -- `media`: A list of links to media files (images, GIFs, mp4, GLB, etc.) to be displayed as the token media. - - On the Enjin Wallet and NFT.io marketplace, when more than one media file is provided, the media is scrollable. -- `fallback_image`: A link to an alternative image file that serves as a backup when the media files listed under `media` fail to load or are unsupported in certain views. - - Best Practice: Use an image hosted on a different server to ensure availability in case of server downtime. - - The `fallback_image` is also helpful for views that do not support the primary media file. - Example: If the main media file is a 3D object (e.g., GLB), a fallback image can be displayed in views that lack 3D rendering support. -- `attributes`: A list of custom properties attached to the token. In the above example, it's used as an indicator of the item's common rarity and a list of games it's usable in. -- `external_url`: A link to the token's website or a website with information on the token. -- `keywords`: A list of keywords relevant to this token. Used to help find this token in search queries. - - In the NFT.io Marketplace, keywords also act as tags. - -:::info Need more information? -For a comprehensive document on the metadata structure, please head over to the [Universal Off-Chain Token Metadata Standard](https://github.com/enjin/universal-metadata-standard/blob/uotm-standard-wip/README.md) page. -::: diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md deleted file mode 100644 index b40d571..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: "Metadata Standard" -slug: "../metadata-standard" -description: "Explore Enjin’s metadata standards, enabling you to define the characteristics of your blockchain tokens and enhance their utility within decentralized applications." ---- -## Best Practices for Collection/Token Metadata - -Metadata plays a vital role in the token ecosystem. It provides necessary details about a token and ensures that tokens are presented correctly across various applications. -To maintain consistency and ensure universal compatibility, it's important to adhere to a specific standard for metadata. - -### Why Follow a Metadata Standard? - -Following a standard for metadata ensures that token details are accurately represented across all applications that support the standard. This eliminates inconsistencies and ensures that your tokens are presented in the way you intended. - -Adhering to a standard also simplifies the process for application developers, as they can easily support tokens that follow the standard. This contributes to the goal of creating a truly interoperable Metaverse. - -For more details on the standard, refer to our [Universal Off-Chain Token Metadata Standard](https://enj.in/metadata). - -### Creating Metadata - -:::info Need an example? -For a complete example of Collection & Token metadata, check out the [Metadata Example page](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-example.md) -::: - -:::warning **Important:** Attribute keys are case sensitive. -Ensure you use the correct casing when defining attributes to avoid errors. -::: - -There are two ways to set metadata for your tokens: - -1. **Off-chain - Hosted JSON File:** Set the collection or token attribute with the key `uri` and the value as a URL to a off-chain hosted JSON file. This file can be hosted either in a centralized manner or on IPFS. - The JSON file should contain metadata that follows the structure defined in the metadata standard. - Example: - ```graphql - mutation SetMetadataUsingJSON { - BatchSetAttribute( - collectionId: 36105 # Specify the collection ID - tokenId: {integer: 0 } # Specify the token ID - attributes: [ - { - key: "uri" - value: "https://yourhost/metadata.json" #Link to your hosted JSON file. - } - ] - ) { - id - method - state - } - } - ``` - -2. **On-chain - Directly Setting Metadata:** Set the metadata directly on-chain using a structure that follows the metadata standard. - Example: - ```graphql - mutation SetMetadataDirectly { - BatchSetAttribute( - collectionId: 36105 # Specify the collection ID - tokenId: {integer: 0 } # Specify the token ID - attributes: [ - { - key: "name" #This attribute key defines the token name, following the metadata standard. - value: "Chronicles of the Celestium" #Token name - }, - { - key: "description" #This attribute key defines the token description, following the metadata standard. - value: "An epic saga where players assume the roles of intrepid tradesmiths, shaping destinies with fire and will across the star-woven expanses of the multiverse." #Token description - } - ] - ) { - id - method - state - } - } - ``` - -Whichever method you choose, it's crucial to ensure the metadata is structured according to the standard to maintain compatibility and present your tokens correctly. -For a detailed guide on adding metadata, please refer to our [Adding Metadata Tutorial](/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md). \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md deleted file mode 100644 index 0e7a7cb..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -title: "Dynamic Metadata" -slug: "dynamic-metadata" -description: "Learn to use Dynamic Metadata to efficiently manage large NFT collections. Our easy-to-follow guide shows how to set one URL for all your tokens, saving time and on-chain costs." ---- - -What if you have a collection with 10,000 unique tokens? Setting up metadata (like the name, image, and description) for each one sounds like a lot of work, and it can be costly. - -**Dynamic metadata** is our simple and efficient solution for this. - -Instead of putting metadata on *every single token*, you just set **one link** on your entire **collection**. Applications like the Enjin Wallet and NFT.io marketplace will then use that single link to automatically find the correct metadata for each specific token as it's needed. - -### 🎯 Why Use Dynamic Metadata? - - * **Save Time & Effort:** Manage metadata for thousands (or millions\!) of tokens from one central place. - * **Save on Costs:** This method uses much less on-chain data, which can reduce the ENJ required to create your tokens. - * **Easy to Update:** Want to change your token's image? Just replace the media file on your server. There's no need for a new on-chain transaction. - ------ - -## How Dynamic Metadata Works - -Understanding this is key to setting it up correctly. - -### How Your Metadata is Found (The Hierarchy) - -When you look at an NFT in your wallet, the wallet needs to find its image and name. It searches in a specific order: - -1. **Token:** First, it checks for metadata *directly on that single token*. -2. **Group (Optional):** If not, it checks if the token is in a *group* that has metadata. -3. **Collection:** If it's still empty, it finally looks at the *whole collection* for the metadata link. - -Dynamic metadata works by setting the link at the **Collection level** (step 3). This way, all tokens in that collection will use this link as their default. - -### The Magic `{id}` Placeholder - -This is the most important part. The single link you set on your collection will include the special placeholder: `{id}.json` - -> **Example link:** `https://my-server.com/metadata/{id}.json` - -When a wallet needs to find a token's details, it automatically replaces `{id}` with the correct filename. Your job is to make sure your files are hosted and named correctly so the wallet can find them. - - -### Let's Walk Through an Example - -Seeing this in action makes it much clearer. - -Let's imagine you have a collection called **"Hero's Hoard"** which has an ID of **`8143`**. -Inside this collection, you have a token called **"Dragon's Fang"** with an ID of **`72`**. - -You have already set up dynamic metadata for your collection, and the `uri` attribute on your collection (`8143`) is: -`ipfs://bafy...xyz/{id}.json` - -Now, a user opens the NFT.io marketplace to look at your "Dragon's Fang" token. Here's what happens behind the scenes: - -1. **NFT.io needs metadata for token `72` in collection `8143`.** -2. It checks the token itself for a `uri`. **Nothing is set there.** -3. It checks the token's group (if any) for a `uri`. **Nothing is set there either.** -4. It checks the *collection* (`8143`) for a `uri`. **Success!** It finds your dynamic link: `ipfs://bafy...xyz/{id}.json`. -5. NFT.io knows it's looking for a *token*, so it uses the token file name convention: `-.json`. -6. It replaces the `{id}` in your link with **`8143-72`**. -7. The final URL it fetches is: `ipfs://bafy...xyz/8143-72.json`. - -Your server (or Pinata, in our example) then sends this specific file, which contains the name "Dragon's Fang" and its unique image. That's how it all connects! - -### What You Must Name Your Files - -For this to work, your hosted files **must** use these exact naming conventions: - - * **For your Collection's metadata:** `.json` - * *Example: `8143.json`* - * **For each Token (NFT) metadata:** `-.json` - * *Example: `8143-72.json` (This is for Token 72 in Collection 8143)* - * **For a Token Group metadata (Optional):** `-group.json` - * *Example: `5-group.json`* - ------ - -## How to Set Up Dynamic Metadata: A Step-by-Step Guide - -This setup is a three-step process. We'll walk you through it using a free, easy-to-use service called [Pinata.cloud](https://www.pinata.cloud/), which hosts files on a decentralized network called IPFS. - -:::tip Do I have to use IPFS? -You can use any public server or hosting service, but Pinata is a great choice for beginners. -::: - -### Step 1: Host Your Media Files (Images, Videos, etc.) - -First, let's get all your images and videos uploaded. - -1. Sign up for a free account at [Pinata.cloud](https://www.pinata.cloud/). -2. On your computer, create a folder and put all your media files into it (e.g., `collection-banner.png`, `token-1.jpg`, `token-2.mp4`). -3. On your Pinata dashboard, click the blue "**+ Add**" button in the top-right corner, then select **Folder Upload**. -4. Upload the folder you just prepared. -5. Once it's finished, you'll see your folder in the file list. Copy its **CID** (a long string of letters and numbers). -6. Save this CID somewhere safe (like a notepad). This is your ``. - - - -### Step 2: Create and Host Your JSON Metadata Files - -Next, we'll create simple text files (called JSON files) that tell the wallet *about* your tokens, including where to find the media files you just uploaded. - -1. **Create your Collection JSON file:** - - * On your computer, create a new text file and name it *exactly* `.json` (e.g., `8143.json`). - * Inside this file, add your collection's details like `name` and `description`, following the [Metadata Standard](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md). - * For the `media` or `image` field, add the URL to your collection's media file from Step 1. The format is: `ipfs:///filename.png` - * *Example:* `"url": "ipfs://bafy...abc/collection-banner.png"` - -2. **Create your Token JSON files:** - - * Now, do the same for *each* token. Create a new text file and name it *exactly* `-.json` (e.Example: `8143-72.json`). - * Inside, add that token's specific `name`, `description`, `attributes`, etc. - * For its `media`, point to its specific media file: `ipfs:///token-72.jpg` - * *Example:* `"url": "ipfs://bafy...abc/token-72.jpg"` - -3. **Create your Group JSON files (Optional):** - - * If you use Token Groups, repeat the process. Name the file `-group.json` (e.Example: `5-group.json`). - -4. **Upload Your JSON Folder:** - - * Once you've created all your `.json` files, put them *all* into a **new, separate folder** on your computer. - * Go back to Pinata and upload this new folder (the one containing all your `.json` files). - * When it's done, copy the **CID** for this **JSON folder**. This is the final piece you need! Let's call it your ``. - -### Step 3: Update Your Collection's On-Chain URI - -Finally, let's tell your collection where to find these new metadata files. You'll do this by setting its `uri` attribute. - -1. The `value` you need to set is your **``** from the last step, followed by the `/{id}.json` placeholder. - * **Example Value:** `ipfs://bafy...xyz/{id}.json` (Replace `bafy...xyz` with your actual ``). -2. Set this attribute using the `BatchSetAttribute` mutation. You can copy/paste the example below into the [graphiql playground](https://platform.enjin.io/graphiql), and change the values. - -```graphql -mutation SetDynamicMetadataURI { - BatchSetAttribute( - collectionId: 8143 # 👈 Change this to your Collection ID - # We're not setting a tokenId, so this applies to the whole collection - attributes: [ - { - key: "uri" - # 👇 Change this to your JSON folder's CID + the {id} placeholder - value: "ipfs://bafy...xyz/{id}.json" - } - ] - ) { - id - state - } -} -``` - -3. Run this mutation, and you're all set\! Wallets and marketplaces will now automatically find your metadata. - -:::warning **Important:** Do Not Replace `{id}` -You must use the literal string `{id}.json` at the end of your URI. **Do not** replace `{id}` with an actual ID. The wallet or marketplace handles this replacement for you. -::: - -:::info -For a more detailed guide on updating metadata using the Enjin Platform, check out [this page](/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md). -::: - ------ - -## Migrating Legacy Metadata (For collections from Ethereum/JumpNet) - -This section is **only** if you are moving a collection from Ethereum or JumpNet and *already* used dynamic metadata there. The token ID format has changed, so you just need to rename your old `.json` files. - -### Conversion Steps - -1. Find the new **Collection ID** for your collection on Enjin Blockchain (e.g., `2000`). -2. Create a new JSON file for your collection, named `.json` (e.g., `2000.json`). -3. Now, for each token, find its **old** `.json` filename. - * *Old Example:* `70800000000000af000000000000000000000000000000000000000000000005.json` -4. **Follow these steps to get the new name:** - 1. **Remove the 32 zeros** from the middle (from position 17 to 48). *Result:* `70800000000000af0000000000000005.json` - 2. **Remove the `.json`** at the end. *Result:* `70800000000000af0000000000000005` - 3. **Convert this number.** This is a "hexadecimal" number. Use any online "hex to decimal" converter to change it. *Result:* `149538149525803038929858507180710297605` - 4. **Add `.json` back** to the end. **New Filename:** `149538149525803038929858507180710297605.json` -5. Repeat this renaming process for all your old token JSON files. -6. Upload all your **newly renamed** files (plus your new collection file, e.g., `2000.json`) into a new folder on your server or IPFS. -7. Finally, update your collection's `uri` attribute (like in [Step 3 above](#step-3-update-your-collections-on-chain-uri)) to point to this new folder, making sure to add `{id}.json` at the end. - * *Example:* `httpss://example.com/metadata/{id}.json` \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/_category_.json deleted file mode 100644 index 6c388d8..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/_category_.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "label": "Metadata Standard" -} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/03-enforced-rarity.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/03-enforced-rarity.md deleted file mode 100644 index 7824cda..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/03-enforced-rarity.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: "Enforced Rarity" -slug: "enforced-rarity" -description: "Learn how to enforce rarity in your blockchain assets using Enjin's features, ensuring controlled distribution and enhanced value of rare tokens." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; - -## NFT Collection - -Consider a situation where your goal is to create a limited to 1,000 tokens, where each token is an symbolizing a distinct in-game sword. - -Accomplishing this requires the right configuration of your Mint Policy: - -![Enforced NFT Collection](/img/guides/advanced-mechanics/enforce-nft-collection.png) - -By setting the "**Max Token Count**" to `1,000`, you can cap the total number of tokens in the swords collection to 1,000. - -Meanwhile, setting the "**Max Token Supply**" to `1` ensures that each token in the swords collection will have a maximum supply of `1`. This enforces the uniqueness of each token in the collection, enforcing each token in the collection to be an NFT. - -## Immutable Tokenomics - -Suppose you want to create a with one simple but crucial rule: Each token has a pre-defined max supply which can only be decreased. -Achieving this requires careful configuration of your Mint Policy: - -

- Enforced Collapsable Supply -

- -By toggling the "**Force Collapsing Supply**", you ensure that each token in the collection will be of collapsing supply type. -This guarantees the immutability of each token's maximum supply in the collection, maintaining its value and scarcity over time. diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/04-brand-collab-nfts.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/04-brand-collab-nfts.md deleted file mode 100644 index 75f6b49..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/04-brand-collab-nfts.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -title: "Brand Collab NFTs" -slug: "brand-collab-nfts" -description: "Discover how to create brand collaboration NFTs, allowing for innovative partnerships that utilize blockchain to engage audiences with exclusive digital assets." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info Co-Authored by [SamTheBay](https://twitter.com/SamTheBay) -Sam is the Founder of [Etherscape](https://www.etherscape.io/), a multiplayer RPG with some of the most innovative Web3 game design elements in the industry. - -[Reach out to Sam on Discord](https://discord.gg/NXwWehg89p) for partnership and collaboration opportunities. -You can see examples of Brand Integrations in [Etherscape's whitepaper](https://whitepaper.etherscape.io/CryptoIntegration/multiverse-support/collaborations). -::: - -As a developer, you can leverage brand integrations and multiverse items - NFTs that work across multiple games - to increase your playerbase and create new monetization methods. - -These NFTs are stored on a public ledger, enabling your game to identify items a player owns, even if they were created elsewhere. - -When a player with a brand or multiverse item enters your game, you can scan their wallet, recognize the item, and integrate it into your game's environment. This approach offers several benefits: - -- Attract players who possess specific items. -- Accumulate these items to draw players interested in obtaining them through your game. -- Get compensated for incorporating these items into your game. -- Consider charging a nominal fee for unlocking the item's features in your game. - -While the concept of multiverse items is appealing for players, it's important for you as a developer to adopt certain development and marketing strategies to ensure you reap measurable benefits from this integration. - -## Acquisition - -Your goal is to inform all existing and potential NFT owners that they can utilize their NFTs in your game, and to give them compelling reasons to engage with your game using these NFTs. - -For this initiative to be successful for you, it's important to implement a range of strategic marketing actions. - -### Show off Your Gameplay and Aesthetics - -Using videos and GIFs is an excellent method to showcase your game's gameplay and visual appeal. They effectively demonstrate the experience players can anticipate when they play your game and utilize the NFTs within it. - -### Create an Exciting Announcement - -Announce that your game has integrated new functionalities for these NFTs. In your announcement, include video footage displaying how the NFT is used in your game, and highlight the enjoyable aspects of your gameplay to engage your audience. - -It's crucial to communicate the reasons behind your decision to integrate these NFTs. Share what aspects of the NFT community you're supporting appeal to you and how your values and vision align with theirs. - -### Ongoing Content Pushes - -Regularly create and share posts, videos, and GIFs that highlight the versatility and capabilities of the NFT, usable across multiple games. This approach not only emphasizes the expanding usefulness of the NFT but also continuously promotes the games that support it. - -### List Your Game on the Token - -Ensure your game is included as a utility provider in the token's metadata. This step guarantees that your game can be found through the token's presence on various wallets and marketplaces. - -### Utility Unlock Challenge - -By designating your game's utility as "locked" on the token and clarifying that players can unlock this utility by engaging with your game, you provide them with a direct incentive to play your game. This strategy motivates players to explore your game in order to access the additional utility of their NFT. - -## Monetization - -To monetize collaborations effectively, it's important to create strategies that are enjoyable for players and advantageous for both the NFT creator and the utility provider. - -### Commissioned Integrations - -The NFT creator can opt to pay a flat fee to cover the costs of integrating their NFT into your platform, including the development of engaging and unique functionalities for the NFT. - -### Community-Commissioned Integrations - -You have the option to set up a community fundraising campaign on platforms like GoFundMe, where a specific funding goal is established. If this goal is met, you'll integrate the item in question. - -The holders of the NFT will likely be inclined to contribute to this fund, as reaching the target means adding additional functionalities to their token. - -### Utility Unlock Fees - -By marking the utility of your game on the token’s metadata as "locked," and clarifying that players can unlock it by paying a small fee, you provide a straightforward incentive for them to pay and access the additional value and functionalities of their NFT in your game. - -## Development - -Here’s the Query you will need to use: - - - -```graphql -query GetWallet { - GetWallet(account:"efUVQhs3Q1ih9RKn1ukpb45p2Zomu5jvopsQE6n5LtW52FJH3") { -// omit “tokenIds” to read all tokens in a collection -// if you are getting a page after the first put the “endCursor” value into the “after” field -tokenAccounts (collectionIds:2100 tokenIds:1 after:"" first:100) { - pageInfo { - hasNextPage - endCursor - } - edges{ - node { - balance - reservedBalance - token{ - tokenId - attributes { - key - value - } - } - } - } - } - } -} - -``` - - -``` -curl -X POST "https://platform.canary.enjin.io/graphql" \ --H "Content-Type: application/json" \ --H "Authorization: enjin_api_key" \ --d '{ - "query": "query GetWallet { GetWallet(account: \"efUVQhs3Q1ih9RKn1ukpb45p2Zomu5jvopsQE6n5LtW52FJH3\") { tokenAccounts(collectionIds: 2100, tokenIds: 1, after: \"\", first: 100) { pageInfo { hasNextPage endCursor } edges { node { balance reservedBalance token { tokenId attributes { key value } } } } } } }" -}' -``` - - - -Here’s an example of a response you can expect. - -```json -{ - "data": { - "GetWallet": { - "tokenAccounts": { - // if there are more items to read there will be a guid in the “endCursor” value - "pageInfo": { - "hasNextPage": false, - "endCursor": "" - }, - "edges": [ - { - "node": { - "balance": "3", - "reservedBalance": "0", - "token": { - "tokenId": "1", - // to read the metadata I should query the url provided as the value for the uri attribute - "attributes": [ - { - "key": "uri", - "value": "https://etherscapegame.com/crypto/ImperialSovereign.json" - } - ] - } - } - } - ] - } - } - } -} - -``` - -## Best Practices - -:::tip Tips - -- Only pull data from the collections you need. This will improve performance and ensure your backend infrastructure remains scalable. -- Always read and check the balance. Sometimes, the player may hold the token but the balance is actually zero, for instance, if the token has been burned. These burned tokens should not have in-game utility. -- Consider whether you want NFTs that are listed on a marketplace to have utility in your game. Items that are listed for sale don't show up in regular token balance and show up in reserved balance instead. If you want them to have utility, you can check the reserved balance and include the reserved supply. -- Consider if you need to use pagination. Users can have hundreds of tokens, in this situation you will need to read them in multiple calls. -- If you plan to utilize metadata from on-chain or external sources, it's important to also read the token's attributes. Typically, you'll find a "uri" attribute that points to the external location of this metadata. -- When accessing external metadata or media, make sure to do so asynchronously and think about storing it in a local cache for faster retrieval. -::: - -### Syncing Tokens - -In order to implement any token in game you will need to sync a list of token ids that your player has in their wallet. In the most simple case, you only need to sync tokens from a single collection (likely the main collection associated with your game). However, in multiverse cases, you will now need to sync tokens from multiple collections. There are a couple of options to achieve this. - -1. Sync All Tokens - The first approach is to sync all the tokens in the players wallet across all collections. This makes the graphQL query very simple. However, with this approach you need to bear in mind that some wallets will have thousands of tokens in them (and could grow to even hundreds of thousands). This means that you can’t rely on syncing all the wallets tokens in a single call. You will need to make sure to implement proper pagination using the GraphQL API’s so that you can read out the tokens in many batches. -2. Sync Specific Collections Only - Another approach is to only sync the collections you care about. This will limit the total number of tokens that you need to sync. In your GraphQL query you can specify a list of collection and token ids in order to narrow down the range. Depending on your case, you may still want to handle pagination of calls if any of the collections you include have a large number of tokens. - -### Syncing Metadata - -When implementing multiverse items you will want to think about how you will represent them in game. This generally means that you are going to want to provide metadata and media for them. Below are a few options for your consideration. - -1. Static Metadata - If you already know exactly what you want the token to do in your game and it won’t change then you can essentially hard code it. In this case, there is no need to read the metadata from the chain or from an external source. If the group and id match your intention then you can give it the in game visuals and utility that you want. -2. External Sources - The most common practice is to store metadata off-chain at an external url. This is especially true for any media content such as images, videos, 3d models and audio. If you want to leverage this metadata then you will need to make calls to the URI that hosts it in order to download it. When doing that, you will want to consider the following… - 1. Make sure the calls are asynchronous. The external calls will take time and you don’t want them to impact your games frame rate. - 2. You probably want to cache any media locally on the drive. The URI you are downloading from could be slow, unreliable or throttle you. As a result, you will want to avoid reading it more often than necessary. - -:::tip Metadata Management -Note that if you’re adopting another game’s token, you may not want to use all the metadata that is provided. For example, if you are making a 2D game you might want to create your own sprite to represent it instead of using a stylistic image that doesn’t suit your game. For example, this is how the ForgeHammer is represented in Etherscape - a 2D pixel-art game. -

- Adjust the item's art style to fit your game -

-::: - -### ERC-1155 Grouped NFTs - -There are many tokens that have been migrated from the ERC-1155 version of Enjin. In ERC-1155 there is a concept called “Grouped NFTs” where there is a grouping of NFT tokens that share the same metadata. - -A popular example of this would be “Oindrasdain” from “The Multiverse” collection. There are many copies of Oindrasdain and they have token ids that look like the following… - -`107002853660685728488179487227240257220` - -If you convert this token id into hex, you will be able to see its 128 bit representation like the following… - -`508000000000002700000000000016C4` - -This can then be broken into two 64 bit parts (or 16 hex string characters) like the following… - -Group ID: `5080000000000027` - -Group Index: `00000000000016C4` - -All tokens in the collection that share the same group id are intended to be of the same token type and thus should get the same utility. This is similar to how s work. diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/05-enj-infusion.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/05-enj-infusion.md deleted file mode 100644 index cabf186..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/05-enj-infusion.md +++ /dev/null @@ -1,224 +0,0 @@ ---- -title: "ENJ Infusion" -slug: "enj-infusion" ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -## Introduction to ENJ Infusion - -ENJ Infusion is an innovative feature of the Enjin Blockchain that allows users to lock Enjin Coin (ENJ) into tokens they create, whether they are s or s. This process effectively embeds ENJ into digital assets, providing them with inherent, tangible value. The infused ENJ can only be retrieved when the token is burned, a process known as "melting," ensuring that the token retains its value until it is destroyed. - -### Why ENJ Infusion is Needed - -ENJ Infusion adds a new layer of value and security to digital assets on the Enjin Blockchain. By tying tokens to a specific amount of ENJ, creators can guarantee a base value for their assets, which can: - -- Enhance user confidence and trust in the token's worth. -- Provide a tangible backing that can incentivize acquiring the token. -- Create a more stable and secure environment for trading and utilizing NFTs and s. - -### Use Cases and Scenarios - -ENJ Infusion can be utilized in various applications and games to enhance user experience and provide additional value: - -1. **Gaming Rewards:** Game developers can infuse ENJ into in-game items, ensuring players that their rewards have real-world value. For example, a rare sword in an RPG game could have 5 ENJ infused into it, making it not just valuable within the game but also outside of it. -2. **Digital Collectibles:** Creators of digital art and collectibles can infuse ENJ into their NFTs, providing buyers with a guaranteed minimum value. This can make digital art more appealing to collectors, knowing that their collectible is backed by ENJ. -3. **Loyalty Programs:** Businesses can create loyalty tokens with infused ENJ, giving customers a tangible value for their loyalty points. For instance, a company could issue tokens with 0.1 ENJ each, which customers can collect and later melt for ENJ. -4. **Crowdfunding and Fundraising:** Projects can issue tokens with infused ENJ to backers, ensuring that their contributions hold value. This can increase trust and participation in crowdfunding campaigns. -5. **In-Game Incentives:** Game developers can incentivize players to spend more time in their games by infusing additional ENJ into their items for performing certain tasks or achieving milestones. - -By integrating ENJ Infusion, creators and developers can provide enhanced value and security for their digital assets, fostering a more robust and trustworthy ecosystem on the Enjin Blockchain. - -*** - -## Creating a Token with Infused ENJ - -To create a token with Infused ENJ, proceed with the instructions on the [Creating Tokens](/02-guides/01-platform/01-managing-tokens/02-creating-tokens/02-creating-tokens.md) page, but make sure to specify the amount of ENJ to infuse into the token, in the “Infuse ENJ” section. -If you are using the `CreateToken` mutation, insert the infusion amount in the `params: infusion` parameter. - -:::warning Calculating the infusion amount -The Platform accepts infusion values in the **base unit** (integers), not decimal ENJ amounts. To calculate the correct input, **multiply your desired ENJ amount by 10^18** (1 quintillion). - - **Formula:** `Desired ENJ infusion` \* `1,000,000,000,000,000,000` - - **Example:** To infuse a token with **5 ENJ**, input `5000000000000000000`. -::: - -### Anyone Can Infuse - -By default, ENJ infusion is restricted only to the collection owner - -At any time, as well as at the time of creating the token, the collection owner can select to to allow anyone to add ENJ infusion to the token. -This can be done with the `CreateToken` mutation by setting the `params: anyoneCanInfuse` parameter to `true`. -If the token is already created, the `anyoneCanInfuse` state can be adjusted via the `MutateToken` mutation, with the `mutation: anyoneCanInfuse` parameter. - -:::warning **Feature Availability Notice** -Currently, the `anyoneCanInfuse` state can only be configured at the time of creating the token. The option to adjust the infusion permission for an existing token via a mutation will be added later on. We will update the documentation once this feature is available. -In the meantime, if you wish to adjust infusion permission of an existing token now, please use the Enjin Console at [console.enjin.io](https://console.enjin.io). -::: - -## Infusing ENJ to existing token - -To add ENJ infusion to a token that already exists, use the `Infuse` mutation: - -:::warning Calculating the infusion amount -The Platform accepts infusion values in the **base unit** (integers), not decimal ENJ amounts. To calculate the correct input, **multiply your desired ENJ amount by 10^18** (1 quintillion). - - **Formula:** `Desired ENJ infusion` \* `1,000,000,000,000,000,000` - - **Example:** To infuse a token with **5 ENJ**, input `5000000000000000000`. -::: - - - -```graphql -mutation Infuse{ - Infuse( - collectionId: 3298 #Specify the collection ID - tokenId: {integer: 1} #Specify the token ID - amount: 5000000000000000000 #Specify the amount of ENJ to infuse - ){ - id - method - state - } -} -``` - - -``` -curl --location 'https://platform.canary.enjin.io/graphql' \ --H 'Content-Type: application/json' \ --H 'Authorization: enjin_api_key' \ --d '{"query":"mutation Infuse(\r\n $collection_id: BigInt!\r\n $token_id: EncodableTokenIdInput!\r\n $amount: BigInt!\r\n) {\r\n Infuse(collectionId: $collection_id, tokenId: $token_id, amount: $amount) {\r\n id\r\n method\r\n state\r\n }\r\n}","variables":{"collection_id":3298,"token_id":{"integer":1},"amount":5000000000000000000}}' -``` - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -// Set up the mutation -var infuse = new Infuse() - .SetCollectionId(3298) // Specify the collection ID. - .SetTokenId(new EncodableTokenIdInput().SetInteger(1)) // Specify the token ID. - .SetAmount(5000000000000000000); // Specify the amount of ENJ to infuse. - -// Define and assign the return data fragment to the mutation -var transactionFragment = new TransactionFragment() - .WithId() - .WithMethod() - .WithState(); - -infuse.Fragment(transactionFragment); - -// Create and auth a client to send the request to the platform -var client = PlatformClient.Builder() - .SetBaseAddress("https://platform.canary.enjin.io") - .Build(); -client.Auth("Your_Platform_Token_Here"); - -// Send the request and write the output to the console. -// Only the fields that were requested in the fragment will be filled in, -// other fields which weren't requested in the fragment will be set to null. -var response = await client.SendInfuse(infuse); -Console.WriteLine(JsonSerializer.Serialize(response.Result.Data)); -``` - - -```cpp -// Coming Soon! -``` - - -```javascript -fetch('https://platform.canary.enjin.io/graphql', { - method: 'POST', - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'}, - body: JSON.stringify({ - query: ` - mutation Infuse( - $collection_id: BigInt! - $token_id: EncodableTokenIdInput! - $amount: BigInt! - ) { - Infuse(collectionId: $collection_id, tokenId: $token_id, amount: $amount) { - id - method - state - } - } - `, - variables: { - collection_id: 3298, //Specify the collection ID - token_id: {integer: 1}, //Specify the token ID - amount: 5000000000000000000 //Specify the amount of ENJ to infuse - } - }), -}) -.then(response => response.json()) -.then(data => console.log(data)); -``` - - -```javascript -const axios = require('axios'); - -axios.post('https://platform.canary.enjin.io/graphql', { - query: ` - mutation Infuse( - $collection_id: BigInt! - $token_id: EncodableTokenIdInput! - $amount: BigInt! - ) { - Infuse(collectionId: $collection_id, tokenId: $token_id, amount: $amount) { - id - method - state - } - } - `, - variables: { - collection_id: 3298, //Specify the collection ID - token_id: {integer: 1}, //Specify the token ID - amount: 5000000000000000000 //Specify the amount of ENJ to infuse - } -}, { - headers: {'Content-Type': 'application/json','Authorization': 'Your_Platform_Token_Here'} -}) -.then(response => console.log(response.data)) -.catch(error => console.error(error)); -``` - - -```python -import requests - -query = ''' -mutation Infuse( - $collection_id: BigInt! - $token_id: EncodableTokenIdInput! - $amount: BigInt! -) { - Infuse(collectionId: $collection_id, tokenId: $token_id, amount: $amount) { - id - method - state - } -} -''' - -variables = { - 'collection_id': 3298, #Specify the collection ID - 'token_id': {'integer': 1}, #Specify the token ID - 'amount': 5000000000000000000 #Specify the amount of ENJ to infuse -} - -response = requests.post('https://platform.canary.enjin.io/graphql', - json={'query': query, 'variables': variables}, - headers={'Content-Type': 'application/json', 'Authorization': 'Your_Platform_Token_Here'} -) -print(response.json()) -``` - - - -Note that you are only allowed to Infuse existing token if you are the collection owner, or if the token's `anyoneCanInfuse` state is set to `True`. diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/06-the-multiverse.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/06-the-multiverse.md deleted file mode 100644 index 62f9571..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/06-the-multiverse.md +++ /dev/null @@ -1,169 +0,0 @@ ---- -title: "The Multiverse" -slug: "the-multiverse" -description: "Learn how to integrate the multiverse into your game or application, allowing users to access cross-game blockchain assets and experiences." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; - -![Using the multiverse](/img/guides/managing-users/using-the-multiverse.jpg) - - is the world's first cross-game item economy, that offers you a powerful growth engine to attract new players into your game and keep them coming back every month. - -When you join, you will be promoted on the [Multiverse Codex](https://enjin.io/multiverse), which acts as an inbound funnel that showcases your game and gives players more reasons to try it. - -These players can then participate in , a monthly event where you can reward them with when they play your game. - -In return, you’re asked to add to the Multiverse Items you distribute, this makes them more desirable to players as they endlessly grow in power. - -This creates an self-sustaining cycle that powers your game's growth: - -1. **Ecosystem-Wide Promotion Fuels Player Attraction**: Multiverse Quests are promoted across the entire ecosystem, drawing in players who are interested in earning valuable cross-game items. This shared onboarding funnel not only boosts individual games but strengthens the whole ecosystem. - -2. **Monthly Rewards Fuel Player Retention**: By offering new rewards each month, players have a continuous reason to return, keeping the community lively and excited. - -3. **Growing Playerbase Attracts More Developers:** As the player community expands, more developers are drawn to the ecosystem, eager to tap into this engaged audience. They adopt the Multiverse items, which in turn gives new and existing players even more reasons to continue playing the existing games to earn these ever-evolving items. - -4. **Utility of Multiverse Items Expands**: As more developers participate, the utility of Multiverse items increases across a wider range of games, making them even more appealing to players and attracting new players into the ecosystem. - -This cycle—driven by consistent player engagement, developer involvement, and expanding item utility—creates a powerful loop that forms a self-reinforcing growth engine. - -## Using the Primythical Chest to Acquire Players - -The is an infinite supply promotional tool and Enjin's primary funnel for onboarding new players into your game. - -Enjin is giving away this item for free, then educating players on where they can use it. - -Players can receive the item by scanning an QR code to get it for free. - -![](/img/guides/managing-users/using-the-multiverse-2.jpg) - -Once they receive the item, they are directed to the [Multiverse Codex](https://enjin.io/multiverse), where they can find games to play where their new item has utility. This is how your game will get discovered. - -Players will also start to discover a handful of other that they can earn by playing yours and other games. - -This will lead them to participate in , where they will play your game every month to earn more Multiverse Items. - -![](/img/guides/managing-users/using-the-multiverse-3.jpg) - -### How to Add Your Game - -To add your game to the [Multiverse Codex](https://enjin.io/multiverse), follow these steps: - -1. Join the [Enjin Spark Program](https://enjin.io/join). -2. Adopt at least five Multiverse Items, including the Primythical Chest. - -In return, your game will be promoted on the [Multiverse Codex](https://enjin.io/multiverse). While Enjin will help drive players to your game, it's your responsibility to convert their interest into lasting engagement. - -Your placement on each item's codex will depend on the quality of your utility and the video content you provide. - -## Using Multiverse Quests to Retain Players - -Multiverse Items are designed as powerful tokens with across multiple games. Both new and existing players are always eager to collect these items, especially as they see their utility expand with adoption by more games. - -Each month, Enjin will supply you with Multiverse Items to attract players eager to earn them in your game. - -Monthly events will feature new opportunities for players to win different Multiverse Items in your game, creating ongoing engagement between you and the Enjin playerbase. - -Over time, players may aim to collect all available items or trade them on [NFT.io](https://nft.io). - -![](/img/guides/managing-users/using-the-multiverse-4.png) - -To enter the quest, players will need to follow these steps: - -**1. Download the [Enjin Wallet](https://enjin.io/technology/wallet)** - -- Receive, secure, and manage your cross-game inventory. Available on [Android](https://play.google.com/store/apps/details?id=com.enjin.mobile.wallet&referrer=utm_source%3Dorganic_software_wallet%26utm_medium%3DOrganic%26utm_term%3Dsoftware_wallet_LP%26utm_content%3DDownload_Icon) or [iOS](https://apps.apple.com/us/app/enjin-cryptocurrency-wallet/id1349078375?ls=1). - -**2. Link Their Enjin Wallet to [Discord](https://enj.in/discord)** - -- Go to the [#link-wallet](https://enj.in/discord) channel in Enjin’s Discord server and follow the prompts. - -**3. Start Their First Multiverse Quest** - -- Find the [#multiverse-quests](https://enj.in/discord) channel in Enjin’s Discord to see a full list of games that are dropping this coveted item. - -![](/img/guides/managing-users/using-the-multiverse-5.jpg) - -### How to Add Your Game - -As new games join The Multiverse, the utility and demand for these items will continue to grow, giving players more motivation to earn them by playing your game. - -To access a supply of Multiverse Items and have Enjin promote your game as a location where players can earn these items, you must: - -1. Join the [Enjin Spark Program](https://enjin.io/join). -2. Adopt at least five Multiverse Items, including the Primythical Chest. -3. Adopt the item featured in the current quest. - -While Enjin helps drive players to your game, it’s up to you to convert their interest into sustained engagement. - -Every month, Enjin will launch a new Multiverse Quest focused on a different item in the collection. - -No new items are planned for this collection, meaning that as more developers enter the ecosystem, they will only need to add utility to the existing items to participate in Multiverse Quests. This will lead to these items growing in utility over time. - -To stimulate adoption of these items, Enjin will mint new supply access each month. Developers use this supply to attract and retain players. However, as long as the utility of the items and the player base in the ecosystem grow faster than the supply of Multiverse Items, the benefits for both developers and players are clear. - -Games with the highest monthly active users will receive a greater share of the reward supply. However, this metric for supply allocation may change in the future. - -Here is the yearly release schedule for Multiverse Items & Quests: - -| Quest | Date | Prize Pool | -| :--- | :--- | :--- | -| The Search for the Shadowsong | October 1st to October 15th | 100 x Shadowsongs | -| The Timeless Quarter of Tramyarus | November 1st to November 15th | 100 x Tramyarus Quarters | -| Mike's Misadventures | December 1st to December 15th | 500 x Mikes | -| The Nethermiser’s Vengance | January 1st to January 15th | 150 x APG-M55s | -| The Mask of the Mage-King | February 1st to February 15th | 100 x Masks of U'thuchul | -| The Blood of the Wanderer | March 1st to March 15th | 200 x Wanderer's Elixirs | -| The Pegasoid's Pursuit | April 1st to April 15th | 100 x Pegasoid Steeds | -| The Soulshift’s Embrace | May 1st to May 15th | 200 x Soulshift Armors | -| The Summoning of the Starbow | June 1st to June 15th | 100 x Starbows | -| Forging the Titan's Hammer | July 1st to July 15th | 350 x Forgehammers | -| The Epoch of the Epochrome | August 1st to August 15th | 400 x Epochrome Swords | -| The Siege of Stormwall | September 1st to September 15th | 250 x Stormwalls | - -## How to Add Multiverse Items into Your Game - -### Step 1: Download the 3D Multiverse Models - -Visit [Enjin's public resource repository](https://drive.google.com/drive/folders/1ijlDn_f9PN8GsT9kQfGZR0ObrUgEL2ge) to download 3D models for Unity and Unreal Engine. - -These ready-to-integrate models will streamline your multiverse integration process. - -You are also welcome to design your own 3D or 2D models to better suit your game's aesthetic, provided they are recognizable to players as the Multiverse Items. - -### Step 2: Add Utility to the Multiverse Tokens. - -To add utility to multiverse items, your game will need to: - -1. [Link player wallets to your game](/02-guides/01-platform/02-managing-users/01-connecting-user-wallets/01-using-wallet-connect.md). -2. [Read the inventory from player wallets](/02-guides/01-platform/02-managing-users/02-reading-user-wallets.md). -3. Validate ownership of the [multiverse items](#the-multiverse-collection). -4. Provide an in-game benefit for each multiverse item. - -Once you have completed these steps, [contact Enjin](https://enjin.io/contact) to get listed on [The Multiverse Codex](https://enjin.io/multiverse) and collaborate on Multiverse marketing initiatives aimed at driving players into your game. - -## The Multiverse Collection - -Collection ID: `2967` - -To see if a user owns a multiverse item, use the [GetTokens query](/03-api-reference/01-queries/03-tokens-queries.md#gettokens) and filter based on Collection ID or Token IDs. - -| Token Name | Token IDs (Ranging From) | TokenIDs (Ranging To) | -| :--- | :--- | :--- | -| [Primythical Chest](https://nft.io/asset/2967-106338239662793273429419659195790131200) | 106338239662793273429419659195790131200 | N/A (/Stackable Tokens) | -| [Aeonclipse Key](https://nft.io/asset/2967-106338239662793274425543839176105918464) | 106338239662793274425543839176105918464 | N/A (/Stackable Tokens) | -| [Archspire](https://nft.io/asset/2967-106338239662793273447866403269499682816) | 106338239662793273447866403269499682816 | N/A (/Stackable Tokens) | -| [Tramyarus Quarter](https://nft.io/asset/2967-106338239662793367710728619925308440576/) | 106338239662793367710728619925308440576 | N/A (/Stackable Tokens) | -| [Epochrome Sword](https://nft.io/asset/2967-107002853660685728525072975374659354626) | 107002853660685728525072975374659354625 | 107002853660685728543519719448368906239 | -| [Forgehammer](https://nft.io/asset/2967-107002853660685728543519719448368909321) | 107002853660685728543519719448368906241 | 107002853660685728561966463522078457855 | -| [Shadowsong](https://nft.io/asset/2967-107002853660685728617306695743207113450) | 107002853660685728617306695743207112705 | 107002853660685728635753439816916664319 | -| [Oindrasdain](https://nft.io/asset/2967-107002853660685728488179487227240254752) | 107002853660685728488179487227240251393 | 107002853660685728506626231300949803007 | -| [Stormwall](https://nft.io/asset/2967-107002853660685728580413207595788010096) | 107002853660685728580413207595788009473 | 107002853660685728598859951669497561087 | -| [Soulshift Armor](https://nft.io/asset/2967-107002853660685728598859951669497561717) | 107002853660685728598859951669497561089 | 107002853660685728617306695743207112703 | -| [APG-M55](https://nft.io/asset/2967-107002853660685728561966463522078458759) | 107002853660685728561966463522078457857 | 107002853660685728580413207595788009471 | -| [Mike](https://nft.io/asset/2967-107002853660685728506626231300949809368) | 107002853660685728506626231300949803009 | 107002853660685728525072975374659354623 | -| [Starbow](https://nft.io/asset/2967-107002853660685760308813014376216789552) | 107002853660685760308813014376216788993 | 107002853660685760327259758449926340607 | -| [The Mask of U'thuchul](https://nft.io/asset/2967-107002853660685751233014930111117393999) | 107002853660685751233014930111117393921 | 107002853660685751251461674184826945535 | -| [Wanderer's Elixir](https://nft.io/asset/2967-107002853660685751251461674184826946411) | 107002853660685751251461674184826945537 | 107002853660685751269908418258536497151 | -| [Pegasoid Steed](https://nft.io/asset/2967-107002853660685760327259758449926341290) | 107002853660685760327259758449926340609 | 107002853660685760345706502523635892223 | diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/_category_.json deleted file mode 100644 index 5f198d3..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/_category_.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "label": "Advanced Mechanics" -} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/_dispatch-transactions-using-fuel-tanks.md b/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/_dispatch-transactions-using-fuel-tanks.md deleted file mode 100644 index 84ef634..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/03-advanced-mechanics/_dispatch-transactions-using-fuel-tanks.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: "Dispatch Transactions Using Fuel Tanks" -slug: "dispatch-transactions-using-fuel-tanks" ---- -BLOCKED BY PLATFORM BUG diff --git a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/01-getting-started.md b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/01-getting-started.md deleted file mode 100644 index 7e86e03..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/01-getting-started.md +++ /dev/null @@ -1,208 +0,0 @@ ---- -title: "Getting Started" -slug: "getting-started" -description: "Learn how to get started with the Enjin SDK, your first step toward building powerful blockchain-based applications and games." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -To have the SDK connect with the Enjin Platform we must use its platform clients and event services. As a starter, we will look over how to set up these clients. - -:::warning Please always make sure to integrate authentication endpoints via secure backend servers. -Direct integration of the SDKs into game clients, which can be decompiled, is strictly not recommended due to potential security risks and exposure of your keys. -::: - -## Platform Clients - -For communicating with the platform's GraphQL API, the SDK defines an `IPlatformClient` interface that works with GraphQL objects for sending requests and receiving responses from the platform. - -### Creating the Client - -The built-in `PlatformClient` class, which implements `IPlatformClient`, utilizes a builder pattern for instantiating new instances. When using this builder, one of the essential inputs we must set is the base address of the platform we will be using. - - - -```csharp -using Enjin.Platform.Sdk; - -IPlatformClient client = PlatformClient - .Builder() - .SetBaseAddress("https://") - .Build(); -``` - - -```cpp -#include "EnjinPlatformSdk/PlatformClient.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -unique_ptr client = PlatformClient::Builder() - .SetBaseAddress("https://") - .Build(); -``` - - - -### Authenticating the Client - -Once we have our client instance we may now authenticate it using an authentication token for our platform. - - - -```csharp -client.Auth(""); -``` - - -```cpp -client->Auth(""); -``` - - - -### Disposing of the Client - -When we no longer need our platform client, we may dispose of it to free up any system resources it may be using with the appropriate method as shown: - - - -```csharp -client.Dispose(); -``` - - -```cpp -// We may reset the pointer or allow the class destructor to handle -// this when our client goes out-of-scope. - -client.reset(); -``` - - - -## Event Services - -The platform broadcasts events that we may respond to or gather information from. For interfacing with an event broadcasting service the SDK offers the `IEventService` interface. Whether we decide to use a free, paid, self-hosted, or any such framework as the driver for broadcasting platform events, the `IEventService` interface defines what operations we may use for working with it. - -### Creating the Service - -The default framework used by the Enjin Platform Cloud and platforms using the Enjin Platform - Starter Template for broadcasting cloud events is [Pusher Channels](https://pusher.com/channels). To work with this framework the SDK has its own `PusherEventService` which implements `IEventService` and serves as an abstraction between us and Pusher for subscribing and listening for events and is a useful tool for those just getting started. - -Our builder for the `PusherEventService` typically expects two attributes to be set before building with a third optional attribute for encryption. The two essential attributes are: - -- The application key -- The hostname of our platform - -:::info -If our application key is not valid, then we may expect a 404 error when connecting our event service. -::: - -The other optional attribute we may set is the encryption status. This will determine which protocol our service will connect with (`wss` or `ws`). If we do not set this attribute, then our builder will default to the `WebSocket Secured` protocol on building. - -An example of building this event service can be seen below: - - - -```csharp -using Enjin.Platform.Sdk; - -IEventService service = PusherEventService - .Builder() - .SetKey("websocket") - .SetHost("") - .SetEncrypted(true) // Defaults to true if not set - .Build(); -``` - - -```cpp -#include "EnjinPlatformSdk/PusherEventService.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -unique_ptr service = PusherEventService::Builder() - .SetKey("websocket") - .SetHost("") - .SetEncrypted(true) // Defaults to true if not set - .Build(); -``` - - - -:::tip -If you wish to change the key (websocket), you can do so in the platform config files. -::: - -::: note -For developers who opt to utilize Pusher's own Channels service for broadcasting events, then instead of `SetHost()` we may use the builder's `SetCluster()` method to connect our event service to the appropriate Pusher cluster. -::: - -### Managing the Connection - -Before we can use our event service, we must connect it to the designated server host through its ConnectAsync method as shown below: - - - -```csharp -using System.Threading.Tasks; - -Task task = service.ConnectAsync(); -``` - - -```cpp -#include - -using namespace std; - -future fut = service->ConnectAsync(); -``` - - - -To disconnect the service from the server, we make a call to its DisconnectAsync method. - - - -```csharp -using System.Threading.Tasks; - -Task task = service.DisconnectAsync(); -``` - - -```cpp -#include - -using namespace std; - -future fut = service->DisconnectAsync(); -``` - - - -## Disposing of the Service - -When we no longer need our event service, we may dispose of it to free up any system resources it may be using with the appropriate method as shown: - - - -```csharp -service.Dispose(); -``` - - -```cpp -// We may reset the pointer or allow the class destructor to handle -// this when our event service goes out-of-scope. - -service.reset(); -``` - - diff --git a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/02-graphql-requests.md b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/02-graphql-requests.md deleted file mode 100644 index b08b74f..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/02-graphql-requests.md +++ /dev/null @@ -1,467 +0,0 @@ ---- -title: "GraphQL Requests" -slug: "graphql-requests" -description: "Learn how to use GraphQL requests to interact with the Enjin platform, allowing you to query and mutate blockchain data efficiently." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -## Creating the Request - -The most important step in sending a request is to first create said request. The SDK provides us with two ways to create a request. The first way is to utilize a predefined, dynamic request class to programmatically compile our request at runtime. The second way is to provide our own query string to be sent along with any relevant information, however, this method of going about creating a request places a greater demand on us to validate that our queries and mutations are indeed correct. - -### Dynamic Requests - -Dynamic requests represent the different queries and mutations we may make in the platform's GraphQL schemas and offer a built-in, programmatic method of arranging requests at runtime. - -### Setting Result Fields - -Requests which produce non-scalar types in the response will have an associated fragment class that may be passed to its aptly named `Fragment` method. For predefined type models in the SDK, there will be corresponding fragment classes that allow us to specify the fields we want to be returned in the response. - -As an example, the `GetCollection` request accepts a `CollectionFragment` instance as its and through the fragment we may specify that we want the `collectId` field to be returned in the data of the response by using a predefined method as shown below: - - - -```csharp -using Enjin.Platform.Sdk; - -CollectionFragment fragment = new CollectionFragment() - .WithCollectionId(); - -req.Fragment(fragment); -``` - - -```cpp -#include "EnjinPlatformSdk/CollectionFragment.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -CollectionFragmentPtr fragment = make_shared(); -fragment->WithCollectionId(); - -req.SetFragment(fragment); -``` - - - -We may also set the fragment to include fields not specified by the SDK if needed by using the general `WithField` method. For this method, we must pass a string as the first parameter containing the name of the field we would like to request and for the second parameter we may either pass a Boolean when requesting a scalar field or an instance of a class implementing which `IGraphQlFragment` for a non-scalar field. An example of this can be seen below: - - - -```csharp -using Enjin.Platform.Sdk; - -CollectionFragment fragment = new CollectionFragment() - .WithField("collectionId", true) - .WithField("owner", new WalletFragment().WithId()); -``` - - -```cpp -#include "EnjinPlatformSdk/CollectionFragment.hpp" -#include "EnjinPlatformSdk/WalletFragment.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -WalletFragmentPtr walletFragment = make_shared(); -walletFragment->WithId(); - -CollectionFragmentPtr fragment = make_shared(); -fragment->WithField("collectionId", true) - .WithField("owner", walletFragment); -``` - - - -:::warning -For requests which accept fragments, a fragment with at least one field requested must be passed or the SDK will throw an exception. -::: - -### Setting Request Variables - -GraphQL requests that have settable variables will have methods prefixed with "set" for setting predefined parameters for the given type. An example of this for the `GetCollection` the query can be seen below: - - - -```csharp -using Enjin.Platform.Sdk; - -GetCollection req = new GetCollection() - .SetCollectionId(2000) -``` - - -```cpp -#include "EnjinPlatformSdk/GetCollection.hpp" -#include "EnjinPlatformSdk/SerializableString.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -SerializableStringPtr value = make_shared("2000"); - -GetCollection req = GetCollection() - .SetCollectionId(value); -``` - - - -Such classes also have a general `SetVariable` method which accepts a string for the parameter name, a string for the variable's type, and a value. This can be used to set variables that are not predefined by the SDK. An example of what this may look like for the `GetCollection` can be seen below: - - - -```csharp -using Enjin.Platform.Sdk; - -GetCollection req = new GetCollection() - .SetVariable("collectionId", "BigInt!", 2000); -``` - - -```cpp -#include "EnjinPlatformSdk/GetCollection.hpp" -#include "EnjinPlatformSdk/SerializableString.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -SerializableStringPtr value = make_shared("2000"); - -GetCollection req = GetCollection() - .SetVariable("collectionId", "BigInt!", value); -``` - - - -Each component of the SDK also comes with a static class containing string fields representing the types present for that component's GraphQL schema and offer a more programmatic option for setting the type of a variable, as shown below: - - - -```csharp -#include "EnjinPlatformSdk/GetCollection.hpp" -#include "EnjinPlatformSdk/SerializableString.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -SerializableStringPtr value = make_shared("2000"); - -GetCollection req = GetCollection() - .SetVariable("collectionId", CoreTypes::BigInt, value); -``` - - -```cpp -#include "EnjinPlatformSdk/GetCollection.hpp" -#include "EnjinPlatformSdk/SerializableString.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -SerializableStringPtr value = make_shared("2000"); - -GetCollection req = GetCollection() - .SetVariable("collectionId", CoreTypes::BigInt, value); -``` - - - -### Setting Input Parameters - -Classes, such as fragments and those modeling a GraphQL input may have settable parameters to be used as part of a request. An example of one of these parameter holders is shown below: - - - -```csharp -using System.Numerics; -using Enjin.Platform.Sdk; - -BigInteger integer = BigInteger.Parse(""); - -EncodableTokenIdInput tokenId = new EncodableTokenIdInput() - .SetInteger(integer); -``` - - -```cpp -#include "EnjinPlatformSdk/EncodableTokenIdInput.hpp" -#include "EnjinPlatformSdk/SerializableString.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -SerializableStringPtr integer = - make_shared(""); - -EncodableTokenIdInputPtr tokenId = make_shared(); -tokenId->SetInteger(integer); -``` - - - -These classes also have a general `SetParameter` method for setting parameters not already defined by the SDK. These parameters may be of a type that implements `IGraphQlParameter` which itself may be a parameter holder, a list of types implementing the aforementioned interface, or a general value representing parameters that do not support nesting such as a scalar. Usage of this method can be seen below: - - - -```csharp -using System.Numerics; -using Enjin.Platform.Sdk; - -BigInteger integer = BigInteger.Parse(""); - -EncodableTokenIdInput tokenId = new EncodableTokenIdInput() - .SetParameter("integer", integer); -``` - - -```cpp -#include "EnjinPlatformSdk/EncodableTokenIdInput.hpp" -#include "EnjinPlatformSdk/SerializableString.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -SerializableStringPtr integer = - make_shared(""); - -EncodableTokenIdInputPtr tokenId = make_shared(); -tokenId->SetParameter("integer", integer); -``` - - - -:::tip -Parameters on the root fragment of a request will be treated as if they are parameters for the request itself, such is the case for the after and first parameters of the ConnectionFragment class. -::: - -### Sending the Request - -When sending requests the SDK offers built-in methods that complement the predefined requests. The methods will be prefixed with "Send" followed by the name of the request. An example of what this may look like for the `GetCollection` the request is shown below: - - - -```csharp -using Enjin.Platform.Sdk; // Namespace of extension method - -// Returns a Task containing the response -client.SendGetCollection(req); -``` - - -```cpp -#include "EnjinPlatformSdk/CoreQueries.hpp" - -using namespace enjin::platform::sdk; - -// Returns a Future containing the response -SendGetCollection(*client, req); -``` - - - -### Static Requests - -Aside from the dynamic queries and mutations built into the SDK, we may pass static query strings and variable mappings to the platform client in cases where speed is valued more than dynamic request creation. - -To facilitate static requests, the platform client implements a method that takes a string for the query body, a nullable mapping for any variables, and a relative path for the request's schema in the API. - -:::warning -The type for the data in the GraphQL query string must be aliased as result to ensure that the returned data may be properly deserialized for the response. -::: - -### Without Variables - -When creating a static request without passing a mapping of variables we need only set the values for such variables in the query string we will be using. After creating our query string we then pass it and the schema path to the `SendRequest` method while leaving the variables argument as `null`, as shown below: - - - -```csharp -using Enjin.Platform.Sdk; - -string query = @"query { - result: GetCollection(collectionId: 2000) { - collectionId - } -}"; -IPlatformRequest req = PlatformRequest - .Builder() - .SetPath("") - .AddOperation(query, null) - .Build(); - -// Returns a Task containing the response -client.SendRequest>(req); -``` - - -```cpp -#include "EnjinPlatformSdk/Collection.hpp" -#include "EnjinPlatformSdk/GraphQlResponse.hpp" -#include "EnjinPlatformSdk/PlatformRequest.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -string query = R"(query { - result: GetCollection(collectionId: 2000) { - collectionId - } -})"; - -PlatformRequestPtr req = PlatformRequest::Builder() - .SetPath("") - .AddOperation(query, {}) - .Build(); - -// Returns a Future containing the response -client->SendRequest>(req); -``` - - - -:::warning -For the C++ SDK, if the request contains an Upload variable then an additional map containing the names and values of the uploads will need to be passed to the AddOperation method. -::: - -### Handling the Response - -Once we have sent our request to the platform we should expect to get a response back with information that we intended to retrieve in the first place or with information that we may act on if we so choose. - -### Getting the Response - -The send methods of a platform client return asynchronous handlers which contain the result of our operation. At the first level of the result, is a platform response of type, which generally holds the HTTP information of the response from the platform. Within this platform response, there is also a `GraphQlResponse` property, which contains the result data of our GraphQL request and any errors that may have been returned with it. - - - -```csharp -using System.Threading.Tasks; -using Enjin.Platform.Sdk; - -GetCollection req = /* Creating the request... */ - -// Send the request and get the Task containing the response -Task>> task = - client.SendGetCollection(req); - -// Get the platform response holding the HTTP data -IPlatformResponse> platformRes = task.Result; - -// Get the result, a GraphQL response, holding the GraphQL data -GraphQlResponse gqlRes = platformRes.GqlResponse; -``` - - -```cpp -#include "EnjinPlatformSdk/Collection.hpp" -#include "EnjinPlatformSdk/CoreQueries.hpp" -#include "EnjinPlatformSdk/GraphQlResponse.hpp" -#include "EnjinPlatformSdk/IPlatformResponse.hpp" -#include -#include - -using namespace enjin::platform::sdk; -using namespace std; - -GetCollection req = /* Creating the request... */ - -// Send the request and get the Future containing the response -future>> fut = - SendGetCollection(*client, req); - -// Get the platform response holding the HTTP data -PlatformResponsePtr> platformRes = fut.get(); - -// Get the result, a GraphQL response, holding the GraphQL data -const optional>& gqlRes = platformRes->GetResult(); -``` - - - -### Processing the Result - -To process the result we must first get the `GraphQlData` property within the `GraphQlResponse`, then we will get the result property from the data. The type of result will be either one of the predefined type models or a scalar value depending on our request. - -The response also has a property indicating whether the request was successful, indicating that a result is present, and we ought to consider checking if this property is `true` before retrieving the result. - - - -```csharp -using Enjin.Platform.Sdk; - -if (gqlRes.IsSuccess) -{ - Collection collection = gqlRes.Data.Result; - - /* Handle the result */ -} -``` - - -```cpp -#include "EnjinPlatformSdk/Collection.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -if (gqlRes.has_value() && gqlRes->IsSuccess()) -{ - const optional& collection = gqlRes->GetData()->GetResult(); - - /* Handle the result */ -} -``` - - - -### Processing Errors - -The response may also contain errors that the platform may return in situations where our request was malformed, had incorrect arguments, or failed some other validation. To retrieve any such errors we make a call to the response's Errors property as shown below: - - - -```csharp -using System.Collections.Generic; -using Enjin.Platform.Sdk; - -if (gqlRes.HasErrors) -{ - IEnumerable errors = gqlRes.Errors; - - /* Handle the errors */ -} -``` - - -```cpp -#include "EnjinPlatformSdk/GraphQlError.hpp" -#include -#include - -using namespace enjin::platform::sdk; -using namespace std; - -if (gqlRes.has_value() && gqlRes->HasErrors()) -{ - const optional>& errors = gqlRes->GetErrors(); - - /* Handle the errors */ -} -``` - - - -The response also has a `HasErrors` property, which indicates whether there are in fact any errors at all and is a property we should check before making an attempt to retrieve them. diff --git a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/03-platform-events.md b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/03-platform-events.md deleted file mode 100644 index 9157055..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/03-platform-events.md +++ /dev/null @@ -1,392 +0,0 @@ ---- -title: "Platform Events" -slug: "platform-events" -description: "Explore how to monitor and handle platform events in the Enjin SDK, ensuring real-time updates for your blockchain applications." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -## Event Listeners - -To allow our application to receive events from the platform the SDK comes with an `IEventListener` interface for us to use to receive events for processing. - -### Creating a Listener - -To use the `IEventListener` interface on a listener class we create we must implement its `OnEvent` method. This method accepts a `PlatformEvent`, which contains the name of the event, the channel it was broadcasted on, the raw event message, and the parsed JSON data representing the message. A generic listener implementing this interface is shown in the code block below: - - - -```csharp -using System.Text.Json; -using Enjin.Platform.Sdk; - -class EventListener : IEventListener -{ - public void OnEvent(PlatformEvent evt) - { - string eventName = evt.EventName; - string channelName = evt.ChannelName; - string message = evt.Message; - JsonElement data = evt.Data; - - /* Handle event */ - } -} -``` - - -```cpp -#include "EnjinPlatformSdk/IEventListener.hpp" -#include "EnjinPlatformSdk/PlatformEvent.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -class EventListener : public virtual IEventListener -{ -public: - void OnEvent(const PlatformEvent& evt) override - { - const string& eventName = evt.GetEventName(); - const string& channelName = evt.GetChannelName(); - const string& message = evt.GetMessage(); - - /* Handle event */ - } -}; -``` - - - -### Registering a Listener - -To make use of the event listener we have created, we will need to register it with the event service so that the service may pass any cloud events it receives to the listener. To do so, we may make a call to the registration method, passing our listener as an argument as can be seen below: - - - -```csharp -IEventListener listener = /* Create event listener */ - -IEventListenerRegistration reg = service.RegisterListener(listener); -``` - - -```cpp -#include "EnjinPlatformSdk/IEventListener.hpp" -#include "EnjinPlatformSdk/IEventListenerRegistration.hpp" - -using namespace enjin::platform::sdk; - -EventListenerPtr listener = /* Make pointer containing event listener */ - -EventListenerRegistrationPtr reg = service->RegisterListener(listener); -``` - - - -By registering our listener this way, the service will pass along any event it receives from the cloud to our listener for handling. - -We may also use the registration the service returned to a way to monitor or validate our listener's state within the service. The registration comes with a getter for the matcher used by the service to filter events, as well as a property indicating whether the listener's registration with the service is still active or not. - -#### Filter Events with Registration - -The event service also provides means for registering listeners while filter out events for the given registration. This may help in the creation of listeners with narrower scopes, such as a dedicated wallet listener. Without using one of the follow methods of registration, a given listener will be notified of all events the service receives. - -One method for registering a listener with event filtering is to pass an event matcher. For any event to be passed along to the listener, the service expects the matcher to return `true` for it. Aside from this requirement the matcher may be configured however we want. - - - -```csharp -using System; -using Enjin.Platform.Sdk; - -IEventListener listener = /* Create event listener */ -Func matcher = evtName => -{ - return /* Return boolean */ -}; - -// Returns a registration -service.RegisterListenerWithMatcher(listener, matcher); -``` - - -```cpp -#include "EnjinPlatformSdk/IEventListener.hpp" -#include "EnjinPlatformSdk/IEventListenerRegistration.hpp" -#include - -using namespace enjin::platform::sdk; -using namespace std; - -EventListenerPtr listener = /* Make pointer containing event listener */ -EventMatcher matcher = [](const string& evtName) { - return /* Return boolean */ -}; - -// Returns a registration -service->RegisterListenerWithMatcher(listener, matcher); -``` - - - -Alternatively, we may pass our listener with an array of events to filter for or exclude using one of two methods as shown below: - - - -```csharp -using System; -using Enjin.Platform.Sdk; - -IEventListener listener = /* Create event listener */ -string[] events = { "", "", "" }; - -// Pass only the listed events to the listener -service.RegisterListenerIncludingEvents(listener, events); - -// Pass events other than the ones listed to the listener -service.RegisterListenerExcludingEvents(listener, events); -``` - - -```cpp -#include "EnjinPlatformSdk/IEventListener.hpp" -#include -#include - -using namespace enjin::platform::sdk; -using namespace std; - -EventListenerPtr listener = /* Make pointer containing event listener */ -std::set events = { "", "", "" }; - -// Pass only the listed events to the listener -service->RegisterListenerIncludingEvents(listener, events); - -// Pass events other than the ones listed to the listener -service->RegisterListenerExcludingEvents(listener, events); -``` - - - -#### Filter Events with Attribute - -Event listeners may also have an attribute attached to them that the service may refer to for filtering. The attribute takes two arguments: a Boolean stating whether the events are allowed and a list of the event names. - - - -```csharp -using Enjin.Platform.Sdk; - -[EventFilter(isAllowed: true, "", "", "")] -class EventListener : IEventListener -{ - /* ... */ -} -``` - - - ```cpp - Work in progress. - ``` - - - -Once the class has been instantiated, we may then register it using the standard `RegisterListener` method that accepts only the listener as an argument. - -:::warning -If a listener with the filter attribute is registered using any of the registration methods that accept filter arguments, then the event service may throw an exception. -::: - -#### The Events to Filter - -The SDK contains utility classes which have constants representing the names of events broadcasted by the platform. These classes allow us to programmatically set which events we want to filter or even act as a simple reference when making branch statements for the different events we receive. An example of usage for one of these utility classes, `SubstrateEvents` can be seen below: - - - -```csharp -using Enjin.Platform.Sdk; - -IEventListener listener = /* Create event listener */ -string[] events = { SubstrateEvents.CollectionCreated }; - -service.RegisterListenerIncludingEvents(listener, events); -``` - - -```cpp -#include "EnjinPlatformSdk/IEventListener.hpp" -#include "EnjinPlatformSdk/SubstrateEvents.hpp" -#include -#include - -using namespace enjin::platform::sdk; -using namespace std; - -EventListenerPtr listener = /* Make pointer containing event listener */ -std::set events = { SubstrateEvents::CollectionCreated }; - -service->RegisterListenerIncludingEvents(listener, events); -``` - - - -#### Unregistering a Listener - -To unregister a listener from the event service, we need only pass the listener instance to the service's unregister method as shown below: - - - -```csharp -// Passing an event listener instance -service.UnregisterListener(listener); -``` - - -```cpp -// Passing an event listener reference -service->UnregisterListener(*listener); // Reference from pointer -``` - - - -We may also unregister all listeners from the service by calling the unregister all method as so: - - - -```csharp -service.UnregisterAllListeners(); -``` - - -```cpp -service->UnregisterAllListeners(); -``` - - - -After a listener is unregistered, the service will also update the `IEventRegistration` we received while registering our listener, so that the registration now returns `false` for its `IsRegistered` getter. - -### Event Subscription - -After setting up our event listener we now need to manage which event channels we want our service to listen to for it to pull events from. This enables us to have our service only listen in on channels necessary for our application's current needs, such as the wallet channel for a connected user, updates to a particular collection, or the progression of a transaction. - -#### Subscribing - -To subscribe to an event channel, we need to pass the channel name to the subscribe method, as shown below: - - - -```csharp -using System.Threading.Tasks; - -string channelName = ""; -Task task = service.SubscribeAsync(channelName); -``` - - -```cpp -#include -#include - -using namespace std; - -string channelName = ""; -future fut = service->SubscribeAsync(channelName); -``` - - - -Once subscribed, the event service will pass along any event broadcasted on the given channel to our registered listeners, so long as said listeners do not exclude the events. - -#### Unsubscribing - -To unsubscribe from an event channel, we may pass the same channel name we used to subscribe, as shown below: - - - -```csharp -using System.Threading.Tasks; - -string channelName = ""; -Task task = service.UnsubscribeAsync(channelName);; -``` - - -```cpp -#include -#include - -using namespace std; - -string channelName = ""; -future fut = service->UnsubscribeAsync(channelName); -``` - - - -We may also unsubscribe from all channels by calling the relevant method. - - - -```csharp -using System.Threading.Tasks; - -Task task = service.UnsubscribeAllAsync(); -``` - - -```cpp -#include - -using namespace std; - -future fut = service->UnsubscribeAllAsync(); -``` - - - -#### Monitoring the Service - -The event service also provides means to monitor its state through events. The following code shows the different events that may be used to receive notifications regarding state changes within the service. - - - -```csharp -// Notifies that the service has connected to a server -service.OnConnected += (sender, _) => { /* ... */ }; - -// Notifies that the service has disconnected froma server -service.OnDisconnected += (sender, _) => { /* ... */ }; - -// Notifies that the service's connection state has updated -service.OnConnectionStateChanged += (sender, state) => { /* ... */ }; - -// Notifies that the service has subscribed to an event channel -service.OnSubscribed += (sender, channelName) => { /* ... */ }; -``` - - -```cpp -PusherEventService::Builder() - // Notifies that the service has connected to a server - .SetOnConnectedHandler([]() { - /* ... */ - }) - // Notifies that the service has disconnected froma server - .SetOnDisconnectedHandler([]() { - /* ... */ - }) - // Notifies that the service's connection state has updated - .SetOnConnectionStateChangedHandler([](ConnectionState state) { - /* ... */ - }) - // Notifies that the service has subscribed to an event channel - .SetOnSubscribedHandler([](const string& channelName) { - /* ... */ - }); -``` - - diff --git a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md deleted file mode 100644 index f80faf4..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "Software Development Kits (SDKs)" -slug: "../software-development-kit" -description: "Get started with the Enjin SDK, offering comprehensive developer tools to integrate blockchain technology into your apps and games with ease." ---- -The Enjin Platform SDKs provide an API for communicating with the Enjin Platform. - -The Enjin Platform SDKs provide a framework that developers can use on their desired programming language as it allows games and apps to interact with the Enjin API without the need of building a bridge between your application and our API requests, the Enjin SDKs facilitate the usage of listening and processing of events emitted by the Enjin Platform, which helps us deliver a better user experience in our application and eliminate unnecessary API requests we would otherwise make to the Enjin Platform. - -## SDK Libraries - -| Library | Link | -| :------ | :------------------------------------------------------------------------- | -| C# | [/enjin/platform-csharp-sdk](https://github.com/enjin/platform-csharp-sdk) | -| C++ | [/enjin/platform-cpp-sdk](https://github.com/enjin/platform-cpp-sdk) | diff --git a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/_category_.json deleted file mode 100644 index 37ba922..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/_category_.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "label": "Software Development Kits (SDKs)" -} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/_logger.md b/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/_logger.md deleted file mode 100644 index d756953..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/04-software-development-kit/_logger.md +++ /dev/null @@ -1,170 +0,0 @@ ---- -title: "Logger" -slug: "logger" ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -## Logger Interface - -The SDK provides a simple logger interface to facilitate logging within its classes and whichever logging framework we choose to use in our application. An example of this interface's implementation can be seen in the block below: - - - -```csharp -using Enjin.Platform.Sdk; - -class Logger : ILogger -{ - public void Log(LogLevel logLevel, string? message) - { - /* Log the message */ - } - - public void Log(LogLevel logLevel, Exception? e, string? message) - { - /* Log the exception and message */ - } -} -``` - - -```cpp -#include "EnjinPlatformSdk/ILogger.hpp" -#include "EnjinPlatformSdk/LogLevel.hpp" -#include -#include - -using namespace enjin::platform::sdk; -using namespace std; - -class Logger : public virtual ILogger -{ -public: - void Log(LogLevel logLevel, const string& message) override - { - } - - void Log(LogLevel logLevel, const exception& e, const string& message) override - { - } -}; -``` - - - -## Log Levels - -The `ILogger` interface uses log levels to communicate the type of message being logged. These levels are represent within the `LogLevel` enums and are summarized in the table below: - -| LogLevel | Description | -| :---------- | :-------------------------------------------------------------------------------------------- | -| Trace | For detailed messages. May expose sensitive information and should not be used in production. | -| Debug | For debugging or messages appropriate in a development environment. | -| Information | General informational messages. | -| Warning | For unexpected behavior that does not necessarily cause a failure. | -| Error | For unhandled errors or exceptions. | -| Critical | For issues that require immediate attention. | -| None | Specifies no messages are to be logged. | - -## Platform Client Logging - -To setup logging for the `PlatformClient` we may pass an `ILogger` instance to its builder as shown below: - - - -```csharp -using Enjin.Platform.Sdk; - -ILogger logger = /* Instantiate class implementing ILogger */ - -// Using a PlatformClient builder -builder.SetLogger(logger); -``` - - -```cpp -#include "EnjinPlatformSdk/ILogger.hpp" - -using namespace enjin::platform::sdk; - -LoggerPtr logger = /* Make pointer containing ILogger implementation */ - -// Using a PlatformClient builder -builder.SetLogger(logger); -``` - - - -## Logging HTTP Traffic - -The SDK also offers HTTP logging options for its built-in platform client. There are four log levels to choose from for HTTP traffic. - -| HttpLogLevel | Description | -| :----------- | :--------------------------------------------------------------------- | -| None | Indicates HTTP traffic is not to be logged. | -| Basic | Logs topical information, such as the method, URI, and content-length. | -| Headers | Logs HTTP headers in addition to topical information. | -| Body | Logs the full request/response. | - -To enable HTTP traffic logging, we must provide an `ILogger` instance and our desired HTTP log level when building the client as shown below: - - - -```csharp -using Enjin.Platform.Sdk; - -ILogger logger = /* Instantiate class implementing ILogger */ -HttpLogLevel level = /* Choose a HTTP log level */ - -// Using a PlatformClient builder -builder.SetLogger(logger) - .SetHttpLogLevel(level); -``` - - -```cpp -#include "EnjinPlatformSdk/HttpLogLevel.hpp" -#include "EnjinPlatformSdk/ILogger.hpp" - -using namespace enjin::platform::sdk; - -LoggerPtr logger = /* Make pointer containing ILogger implementation */ -HttpLogLevel level = /* Choose a HTTP log level */ - -// Using a PlatformClient builder -builder.SetLogger(logger) - .SetHttpLogLevel(level); -``` - - - -### Event Service Logging - -To setup logging for the `PusherEventService` we may pass an `ILogger` instance to its builder as shown below: - - - -```csharp -using Enjin.Platform.Sdk; - -ILogger logger = /* Instantiate class implementing ILogger */ - -// Using a PusherEventService builder -builder.SetLogger(logger); -``` - - -```cpp -#include "EnjinPlatformSdk/ILogger.hpp" - -using namespace enjin::platform::sdk; - -LoggerPtr logger = /* Make pointer containing ILogger implementation */ - -// Using a PusherEventService builder -builder.SetLogger(logger); -``` - - diff --git a/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md b/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md deleted file mode 100644 index e6deb90..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/01-setup-guide.md +++ /dev/null @@ -1,179 +0,0 @@ ---- -title: "Enjin Farmer: Setup Guide" -sidebar_label: "Setup Guide" -slug: "setup-guide" -description: "Follow our step-by-step guide to set up a sample Unity game with full Enjin NFT integration. Learn how to configure a client, server, and wallet to mint, transfer, and manage blockchain assets in a hands-on project." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; - -Welcome! This guide will walk you through setting up and running the Enjin Farm Game, a sample project demonstrating how to integrate Enjin's NFT technology into a Unity game. - -In this simple farming game, you'll plant seeds, harvest crops, and collect resources. Some resources are special and will be minted as NFTs directly to your in-game wallet. You can then view these NFTs in your inventory, them, or transfer them to an external wallet. - -### System Architecture - -The project consists of four main components that work together: - - - **[Game Client (Unity)](https://github.com/enjin/platform-sample-game-client-unity):** The game itself, where you play and interact with items. - - **[Game Server (Node.js)](https://github.com/enjin/platform-sample-game-server):** A backend API that the game client communicates with to handle all NFT-related actions like minting and transferring. - - **:** The cloud-based service that provides the core NFT infrastructure. - - **Wallet Daemon:** A secure application that manages a wallet on behalf of the game to automatically sign and approve transactions. - ------ - -## Prerequisites - -Before you begin, make sure you have the following installed: - - - ✅ **Unity Hub** with **Unity Editor version `6000.0.24f1`**. - - ✅ **Node.js** (which includes `npm`). - - ✅ **Git** for cloning the repositories. - - ✅ An **Enjin Platform account**. If you don't have one, you can create it [here](https://platform.canary.enjin.io/). - - ✅ Some cENJ tokens (can be aquired from the [cENJ Faucet](https://faucet.canary.enjin.io/)) - ------ - -## Step 1: Download Project Files - -First, you need to download the game client, the game server, and the Wallet Daemon. - -1. **Clone the Game Client:** Open a terminal or command prompt and run: - - ```bash - git clone https://github.com/enjin/platform-sample-game-client-unity.git - ``` - -2. **Clone the Game Server:** In the same terminal, run: - - ```bash - git clone https://github.com/enjin/platform-sample-game-server.git - ``` - -3. **Download the Wallet Daemon:** Download the latest executable for your operating system from the [Wallet Daemon releases page](https://github.com/enjin/wallet-daemon-ui/releases). - ------ - -## Step 2: Configure Enjin Services - -Next, you'll set up your Enjin Platform account and the Wallet Daemon. - -### Enjin Platform - -1. Log in to your [Enjin Platform](https://platform.canary.enjin.io/) account. -2. Head over to your [account settings page](https://platform.canary.enjin.io/settings). -3. Navigate to the **Daemon Wallet** section and create a new API Token. -4. Copy the **API Token**; you will need this in the next step. - -### Wallet Daemon - -1. Run the Wallet Daemon executable you downloaded. -2. Follow the on-screen instructions to configure it, and enter the **API Token** you just copied from the Enjin Platform into the Settings → Canary Matrixchain input field. For a detailed guide, see the [Wallet Daemon documentation](https://docs.enjin.io/getting-started/using-wallet-daemon#wallet-daemon-executable). -3. Select the Enjin Platform Canary in the network dropdown menu, and run the Wallet Daemon. -4. Once configured and running, the Wallet Daemon UI will display a wallet address. **Copy this wallet address** for the next step. - ------ - -## Step 3: Configure and Run the Game Server - -Now, let's set up the backend server that powers the game's NFT features. - -1. Navigate into the game server directory you cloned: `cd platform-sample-game-server`. -2. Duplicate the `.env.example` file and rename the copy to `.env`. -3. Open the `.env` file and fill in the following variables: - - `PORT=3000` (You can change this if port 3000 is already in use). - - `JWT_SECRET`: Generate another secure, random string. This is used for authenticating players. - - `ENJIN_API_URL`: Keep the default `https://platform.canary.enjin.io/graphql` for testing on the Canary network. - - `ENJIN_API_KEY`: Paste the **API Key Token** from your Enjin Platform account. - - `DAEMON_WALLET_ADDRESS`: Paste the wallet address you copied from the Wallet Daemon UI. - - `ENJIN_COLLECTION_ID`: Leave this blank for now. -4. Install the server dependencies by running `npm install`. -5. Launch the server for the first time by running `npm run dev`. - -The server will now connect to the Enjin Platform, create a new NFT collection for your game, and create the NFT tokens for the in-game resources. - -:::info **Important** -Watch the terminal logs. Once the setup is complete, the server will log the new **Collection ID**. It will look something like this: `Collection and resource tokens are ready. Using collection ID: XXXXXX`. **Copy this Collection ID** and save it. You'll need it to configure the game client. -::: - -Keep the server and the Wallet Daemon running in the background. - ------ - -## Step 4: Configure the Unity Game Client - -It's time to set up the Unity project and connect it to your game server. - -1. Open **Unity Hub**. -2. Click `Add` → `Add project from disk` and select the `platform-sample-game-client` folder you cloned earlier. -3. Open the project. -4. Once the project is open in the Unity Editor, you need to configure two things. - -#### 1. Configure the NFT Items - - - In the `Project` window, navigate to `Assets/Enjin Integration/Scripts/Data/Items`. - - You will see three `Enjin Item` assets: `GemGreen`, `GoldCoin`, and `GoldCoinBlue`. -

- -

- - Click on **each one** of these items. - - In the `Inspector` window for each item, find the **Collection Id** field and paste the `Collection ID` you saved from the game server's terminal log. -

- -

- -#### 2. Configure the connection to the Game Server - -:::note -If you are running the game server and client on the same machine, and you haven't changed the default port of 3000, you can skip this step. -::: - - - In the `Project` window, navigate to `Assets/Enjin Integration/Prefabs/`. - - Select the **EnjinManager** prefab. - - In the `Inspector` window, find the `Enjin API Service (Script)` component and configure the host property to your game server URL. If you are running the game server and client on the same machine, the default `http://localhost:3000` is correct. - -

- -

- ------ - -## Step 5: Play the Game! 🎮 - -You're all set up and ready to play. - -1. In the Unity Editor's `Project` window, navigate to `Assets/HappyHarvest/Scenes`. -2. Double click on the `Farm_Outdoor` scene. -3. Press the **Play** button at the top of the Unity Editor to launch the game. -4. Look at the **Console** window in the editor. You should see a log message: `Server connection successful (Health Check: OK)`. -

- -

- :::warning - If you see an error, double-check that your server is running and that the `Host` and `App Key` in the `EnjinManager` are correct. - ::: -5. In the game, click the **Menu** button (top-right), then **Login**. -

- -

-6. Enter an email and password and click **Login**. This will register a new user and create a managed wallet for your player on the Enjin Platform. -7. Close the menu and use the **W, A, S, D** keys to move your character. -8. Walk up to a crop and click on it to harvest it. Keep harvesting until a resource item pops out. -

- -

-9. Click on the resource item to collect it. This action tells the game server to mint that item as an NFT to your player's wallet. -10. From the inventory, you can click **Melt** to destroy the NFT or enter another wallet address in the **Transfer Recipient** field and click **Send** to send it to someone else. -

- -

-:::warning cENJ funds are required -New managed wallet have no funds. To melt or transfer tokens out of a managed wallet, you'll need to fund it with some cENJ, or set up a fuel tank. -To receive cENJ funds for testing, use the [cENJ faucet](https://faucet.canary.enjin.io/) -::: - -:::info Understanding the code -To learn more about the implementation and dive deep into the game client and server code, continue to the [Enjin Farmer: Implementation Breakdown page](/02-guides/01-platform/05-enjin-farmer-sample-game/03-implementation-breakdown.md). -::: - -Happy farming! \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/03-implementation-breakdown.md b/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/03-implementation-breakdown.md deleted file mode 100644 index b59ebc0..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/03-implementation-breakdown.md +++ /dev/null @@ -1,185 +0,0 @@ ---- -title: "Enjin Farmer: Implementation Breakdown" -sidebar_label: "Implementation Breakdown" -slug: "implementation-breakdown" -description: "Dive deep into the code and architecture of the Enjin Farmer sample game. This technical breakdown explains the implementation flow, from the Unity client to the Node.js game server. Understand the key GraphQL mutations and API calls used to mint, transfer, and manage NFTs in your game." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; - -The **Enjin Farmer** sample project demonstrates a basic Enjin Platform integration within a Unity game. It's built with a client-server architecture to ensure security and scalability. - -The project consists of two main components: - - * **🎮 Unity Game (Client):** The front-end game that players interact with. It handles gameplay, visuals, and user input, communicating with the game server to perform blockchain actions. - * **🖥️ Game Server (Backend):** A Node.js application that manages all Enjin Platform logic. It securely handles wallet creation, token minting, and other on-chain operations on behalf of the players. - -### 💡 Important Considerations - -Before you begin, please keep the following in mind: - - * **Demonstration Purpose:** This is a simplified example designed to showcase a basic integration. It is **not suitable for a production environment** as is. - * **WebSockets:** This implementation does not use [WebSocket events](/03-api-reference/03-websocket-events.md). In a real-world application, WebSockets can simplify the process of listening for transaction finalization and receiving real-time updates, such as when a user receives an NFT from an external source like the marketplace. - * **Wallet Funding:** New managed wallets are created without any funds. To cover network fees for actions like melting or transferring tokens, you must either fund each wallet individually or use a [Fuel Tank](/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md) to subsidize transactions for all your users. - ------ - -## 🖥️ Game Server - -The game server is a RESTful API built with Node.js and Express. It serves as the secure bridge between the game client and the Enjin Platform. The main entry point is the [`src/index.js` file](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/index.js). - -### Environment Variables - -The server is configured using the following environment variables: - -| Variable | Description | -| :--- | :--- | -| `PORT` | The port the server listens on. Defaults to `3000`. | -| `JWT_SECRET` | A secure, random string used for signing player authentication tokens. | -| `ENJIN_API_URL` | The Enjin Platform API URL. Use `https://platform.canary.enjin.io/graphql` for testing (Canary Network) or `https://platform.enjin.io/graphql` for production. | -| `ENJIN_API_KEY` | Your API Key Token obtained from the Enjin Platform. | -| `DAEMON_WALLET_ADDRESS` | The address of your Wallet Daemon. This wallet receives the initial supply of all created tokens. | -| `ENJIN_COLLECTION_ID` | The ID of the Enjin Farmer collection. If left blank, the server will create a new collection on startup. | - -### Server Initialization & Collection Setup - -On startup, the server performs a one-time setup to ensure the necessary blockchain assets exist. - -1. **Check for Collection:** The server first checks if an `ENJIN_COLLECTION_ID` has been provided. -2. **Create Collection:** If the ID is missing, the server calls the [`createCollection` function](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L69), which executes the `CreateCollection` mutation on the Enjin Platform. The automatically signs the request. - ```graphql - mutation CreateCollection($name: String!, ...) { - CreateCollection(...) { - id - method - state - } - } - ``` -3. **Monitor Transaction:** The mutation returns a request ID. The server then [polls the `GetTransaction` query](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L15) until the transaction `state` is `FINALIZED` and the `result` is `EXTRINSIC_SUCCESS`. - ```graphql - query GetTransaction($requestId: Int!) { - GetTransaction(id: $requestId) { - state - result - events { ... } - } - } - ``` -4. **Extract Collection ID:** Once finalized, the server [extracts the new collection ID from the transaction's events](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L43) and assigns it to the `ENJIN_COLLECTION_ID` variable. -5. **Create NFTs:** Using a similar process, the server then [creates the three resource NFTs](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L116) ([Gold Coin](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L6), [Gold Coin (Blue)](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L7), and [Green Gem](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L8)) within the collection by calling the `CreateToken` mutation for each and waiting for finalization. - ```graphql - mutation CreateToken($collectionId: BigInt!, $name: String!, ...) { - CreateToken(collectionId: $collectionId, params: { ... }) { - id - method - state - } - } - ``` - -After this setup is complete, the server starts listening for API requests. - -### API Endpoints - -The server exposes several endpoints to handle game actions. The `wallet` and `token` endpoints are protected by a [JWT authentication middleware](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/middlewares/jwtAuth.js), which verifies the player's identity before processing the request. - -#### Authentication - - * [`GET /api/auth/health-check`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/auth.js#L7): A simple endpoint to verify that the server is online. - * [`POST /api/auth/register`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/auth.js#L14): Creates a new player and an associated [managed wallet](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md). To create the wallet, it calls the [`CreateWallet` mutation](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L271), using the player's [email address as the unique `externalId`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/auth.js#L18). - ```graphql - mutation CreateWallet($externalId: String!) { - CreateWallet(externalId: $externalId) - } - ``` - * [`POST /api/auth/login`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/auth.js#L36): Logs in an existing player, returning their wallet address and a JWT. - -#### Wallet Management - - * [`POST /api/wallet/create`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/wallet.js#L28): Creates a new managed wallet - * [`POST /api/wallet/get`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/wallet.js#L8): Retrieves details for the authenticated player's managed wallet. - * [`GET /api/wallet/get-tokens`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/wallet.js#L48): Retrieves the player's managed wallet and all tokens it holds. It calls the [`GetWallet` query](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L208) (using [GraphQL Pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination) to loop through all pages of results, ensuring the complete inventory is fetched). - ```graphql - query GetWalletTokens($externalId: String!) { - GetWallet(externalId: $externalId) { - account { ... } - tokenAccounts(...) { ... } - } - } - ``` - -#### Token Actions - - * [`POST /api/token/mint`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/token.js#L8): Mints a token to the player's wallet. The server calls the [`MintToken` mutation](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L317). The recipient address is extracted from the player's authenticated session. - ```graphql - mutation mintToken($recipient: String!, $collectionId: BigInt!, ...) { - MintToken(recipient: $recipient, collectionId: $collectionId, ...) { - id - } - } - ``` - * [`POST /api/token/melt`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/token.js#L29): Melts a token from the player's wallet. This uses the [`Burn` mutation](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L371). Since the token needs to be melted from the managed wallet's account, it needs to be signed by the managed wallet. For that, the player's managed wallet address is specified as the `signingAccount`. ([Learn more about managed wallets here](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md)) - ```graphql - mutation burnToken($signingAccount: String!, $collectionId: BigInt!, ...) { - Burn(signingAccount: $signingAccount, collectionId: $collectionId, ...) { - id - } - } - ``` - * [`POST /api/token/transfer`](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/routes/token.js#L50): Transfers a token from the player's managed wallet to another address. This uses the [`SimpleTransferToken` mutation](https://github.com/enjin/platform-sample-game-server/blob/4b84cf06df32bf2230197ffad14b2c7e0884e1f4/src/services/enjinService.js#L425), again specifying the player's managed wallet as the `signingAccount`. - ```graphql - mutation transferToken($signingAccount: String!, $recipient: String!, ...) { - SimpleTransferToken(signingAccount: $signingAccount, recipient: $recipient, ...) { - id - } - } - ``` - ------ - -## 🎮 Unity Game - -The Unity game is the client-facing part of the project. It focuses on gameplay and user experience while offloading all sensitive blockchain operations to the game server. - -### Core Components - -The Enjin integration is managed by a few key scripts and a central prefab: - - * **`EnjinManager.prefab`**: The heart of the integration. This prefab is added to the `Farm_Outdoor` scene and configures the **Host URL** (e.g., `http://localhost:3000`) in the Inspector to connect to your game server. - * **[`EnjinManager.cs`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs)**: A singleton controller that manages the player's session (auth token, wallet data) and exposes high-level methods like `MintToken()` for other game scripts to use. - * **[`EnjinApiService.cs`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/API/EnjinApiService.cs)**: Handles all REST API communication with the game server using Unity's `UnityWebRequest`. - * **[`EnjinItem.cs`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Data/EnjinItem.cs)**: A `ScriptableObject` that represents the data of a blockchain item, such as its display name and its corresponding on-chain token ID. - * **UI Scripts** ([`BackpackUI.cs`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/UI/BackpackUI.cs), [`BackpackItemController.cs`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/UI/BackpackItemController.cs)): Scripts that manage the UI for viewing and interacting with the player's NFT inventory. - -### Initial Setup & Player Authentication - -1. **Health Check**: On launch, the client [calls the `/api/auth/health-check` endpoint](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/API/EnjinApiService.cs#L36) to ensure the server is available. -2. **Login/Register**: From the login screen, the player [clicks "Login"](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/HappyHarvest/Common/UI/SettingMenu/Script/SettingMenu.cs#L145), which calls the [`EnjinManager.Instance.RegisterAndLogin()` method](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs#L141). -3. **API Request**: This triggers `EnjinApiService` to [send a POST request to the `/api/auth/register` endpoint](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/API/EnjinApiService.cs#L69). -4. **Store Auth Token**: The server responds with a JWT authentication token. The client [saves this token locally using `PlayerPrefs`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs#L152) and [loads it on subsequent launches](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs#L40) for a seamless experience. - -### In-Game NFT Interactions - -All blockchain actions are initiated by the client but securely executed by the server. - -#### Harvesting and Minting Tokens - -When a player [harvests a crop with the Hoe tool](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/HappyHarvest/Scripts/Items/Hoe.cs#L18), they have a chance to find a resource token. - -1. An `EnjinToken` GameObject [appears on the harvested tile](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs#L218). -2. When the player collects this GameObject, its [`InteractedWith()` method](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Gameplay/EnjinToken.cs#L20) is triggered. -3. This calls [`EnjinItem.Collect()`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Data/EnjinItem.cs#L37), which in turn calls `EnjinManager.Instance.MintToken()`. -4. `EnjinManager` then uses `EnjinApiService` to [send a request to the `/api/token/mint` endpoint](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/API/EnjinApiService.cs#L97). - -#### Viewing the Wallet (Backpack UI) - -1. Clicking the backpack icon opens the inventory screen, managed by `BackpackUI.cs`. -2. The UI [calls `EnjinManager.Instance.GetManagedWalletTokens()`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/UI/BackpackUI.cs#L47), which [sends a request to the `/api/wallet/get-tokens` endpoint](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/API/EnjinApiService.cs#L205). -3. The server saves the list of tokens, and the `BackpackUI` [populates the view with the data](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/UI/BackpackUI.cs#L81). -4. The `BackpackUI` also [subscribes to the `EnjinManager.Instance.OnWalletUpdated` event](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/UI/BackpackUI.cs#L39) to automatically refresh the inventory after a token is [minted](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs#L95), [melted](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs#L107), or [transferred](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs#L119). - -#### Melting and Transferring Tokens - - * **Melting**: The player [clicks "Melt" in the backpack](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/UI/BackpackItemController.cs#L38). This flows through [`EnjinManager`](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/Core/EnjinManager.cs#L100) and sends a request to the [`/api/token/melt` endpoint](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/API/EnjinApiService.cs#L126). - * **Transferring**: The player [clicks "Send"](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/UI/BackpackItemController.cs#L59), sending a request to the [`/api/token/transfer` endpoint](https://github.com/enjin/platform-sample-game-client-unity/blob/e154e83723a76861ca762f4b998ef8c8a44ee44f/Assets/Enjin%20Integration/Scripts/API/EnjinApiService.cs#L155). \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/_category_.json deleted file mode 100644 index d3d632f..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/05-enjin-farmer-sample-game/_category_.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "label": "Enjin Farmer Sample Game" -} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/01-platform/06-enjin-discord-bot.md b/versioned_docs/version-2.x/02-guides/01-platform/06-enjin-discord-bot.md deleted file mode 100644 index 331e922..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/06-enjin-discord-bot.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: "Enjin Discord Bot" -slug: "enjin-discord-bot" -description: "Discover how to use the Enjin Discord Bot to integrate blockchain functionality within Discord servers, enhancing community interaction." ---- -
- -
---- -The Enjin Discord Bot offers an advanced integration solution, enabling users to seamlessly connect their wallets. Utilizing this secure wallet connection, the bot is capable of automatically assigning specific roles based on the user's collection and NFT holdings. - -## Step #1: Add the Enjin Bot to your discord server - -Head over to this page to add the Enjin Bot to your discord server: https://enj.in/discord-bot - -## Step #2: Grant the Enjin Bot permissions - -Navigate to "**Server Settings**" within your Discord server, and select "**Roles**". -For the Enjin Bot to be able to assign roles to users, it must be positioned higher in the hierarchy than all the roles it is intended to assign. -In the roles list, locate the Enjin Bot and drag it **above** the roles it will be managing. -Make sure to save the changes by clicking the "**Save Changes**" button. - -![Setting up the Enjin Discord Bot - Configuring Roles Permissions](/img/guides/integrations/discord-roles-permissions.gif) - - -## Step #3: Setup the role assignment configurations - -To configure the role assignment, you will need to enter the specific collection IDs that the Enjin Bot will use to assign roles. - -In your Discord server, type `/setup ` or type `/` and select the `/setup` command. You will be prompted to enter the collection ID, and two optional fields `asset` ID and `balance` (which can be viewed by pressing the 'Tab' button) to assign role based on token holdings. -These IDs correspond to the collection / asset that users must own in order to be assigned certain roles. - -You can find the collection IDs by visiting [NFT.io](https://nft.io) and selecting the specific collection. The collection ID is located next to the collection name or in the token's details, where you can also find the token ID. - -After verifying and finalizing the configuration settings, send the message. - -The bot will then prompt you to select which roles to assign - Select the roles and click anywhere outside of the selection box to confirm the selection. - -![Setting up the Enjin Discord Bot - Running the Setup Command](/img/guides/integrations/discord-running-setup-command.gif) - -:::tip Did you know? -You can assign different roles based on token amount holdings. -e.g. `Enjineer` role if owning at least 1 Blobby token, and `Gamer` role if owning at least 10 Blobby tokens. -::: - -## Step #4: Configure the Wallet Connection Channel - -To enable the bot to scan user wallets, users must first establish a connection between their wallets and your Discord server. This requires a dedicated text channel for wallet connections. - -Create a specific text channel that users will utilize for this purpose. - -Within this newly created channel, execute the `/add-button` command. This command will generate a user-friendly button for users to connect their wallets. - -For a more personalized user experience, you may also add a custom description to the wallet connection button. Simply append the `description` parameter to the `/add-button` command prompt. - -![Setting up the Enjin Discord Bot - Configuring Wallet Connection Channel](/img/guides/integrations/discord-setting-up-wallet-connection.gif) - -*** - -Congratulations! You have successfully configured the Enjin Discord Bot for your server. This advanced integration will allow for seamless wallet connection and role assignment based on user's collection and NFT holdings. - -For those interested in further customization or hosting options, the Enjin Discord Bot code is open-source and publicly available at https://github.com/enjin/discord-bot. Feel free to explore, alter, and host it according to your specific needs. diff --git a/versioned_docs/version-2.x/02-guides/01-platform/_category_.json b/versioned_docs/version-2.x/02-guides/01-platform/_category_.json deleted file mode 100644 index a50d670..0000000 --- a/versioned_docs/version-2.x/02-guides/01-platform/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "Platform", - "link": { - "type": "generated-index", - "description": "Dive into guides for the Enjin Platform. Learn to manage tokens, users, and leverage advanced features. Use the SDKs for seamless blockchain integration into your project." - } -} - \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/02-staking-integrtion-guide.md b/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/02-staking-integrtion-guide.md deleted file mode 100644 index c0ed41c..0000000 --- a/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/02-staking-integrtion-guide.md +++ /dev/null @@ -1,669 +0,0 @@ ---- -title: "Staking Integration Guide" -slug: "staking-integration" -description: "This page will cover the basic integration of Enjin Blockchain Staking functionality, enabling you and your users to get started with staking on the Enjin Blockchain." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; - -## Background - -[Enjin Blockchain](https://enjin.io/enjin-blockchain) is the world's first purpose-built blockchain for games and apps. Enjin is a Proof-of-Stake dual-layer chain with NFT-specific functions at the protocol level. - -Enjin Blockchain is built on-top of [Substrate](https://substrate.io/) and inherits similar staking functionalities to other Relaychains running on Substrate, with a few key differences which will be covered throughout this Integration Guide. - -## Definitions - -There are a series of definitions that’ll be used throughout this document. - -“**[Validator](/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md)**” refers to a blockchain node that is setup to produce and validate block production. - -“**Era**” refers to a Validator session. During this session, a set of Validators are elected and work together for the integrity of the blockchain. An Era, on the Enjin Relaychain, is 14,400 blocks — it is commonly considered to be “_an era is one (1) full day._” - -“**Active Validator**” refers to a Validator which has been selected to produce work in a given Era. - -“**ENJ**” refers to the native token of the Enjin Blockchain: [Enjin Coin](https://enjin.io/enjin-coin). - -**** refers to a with multiple quantities, created using the [multiToken pallet](/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md). - -“**NFT**” refers to a Non-Fungible Token created using the [multiToken pallet](/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md). - -“**Staking**” refers to the process of bonding (locking) ENJ on the Enjin Relaychain for a minimum of twenty-eight (28) eras. Staking is used to select Active Validators who will be responsible for maintaining the integrity of the blockchain. - -“**sENJ**” refers to Staked Enjin Coin, an . This represents ENJ that is locked into a Nomination Pool. The collection identifier for this token is **1**. - -“**Liquid Staking**” refers to a concept which allows users to use their sENJ for purposes such as governance or [exchanging](/04-enjin-blockchain/02-enjin-relaychain/02-stake-exchange-pallet.md). - -“**Nomination Pool”** refers to a mechanism that allows a group of users to stake their ENJ and nominate a set of Validators that represents the group. Specifically, the nominationPools pallet. - -“**Degen**” refers to an NFT that is used for the purpose of creating and managing a Nomination Pool. The collection identifier for this token is **2**. - -“**Inactive Degen**” refers to a Degen that has not yet been assigned to a Nomination Pool. - -“**Pool Member**” refers to an account that has contributed (staked) some amount of ENJ into a given Nomination Pool. - -“**Pool Owner**” refers to the account that currently holds the Degen associated with a Nomination Pool. - -“**Staker**” refers to an account that has contributed (staked) some amount of ENJ into any Nomination Pool. - -“**Stake Exchange**” refers to a pallet that is responsible for users being able to exchange their sENJ for ENJ based on offers defined by the users. - -“**Stake Factor”** refers to a Nomination Pool’s total bonded ENJ divided by the total supply of sENJ. This value is multiplied against your sENJ contribution, for that Nomination Pool, to derive the total amount of ENJ unbondable. - -“**Pool Deposit**” refers to the minimum of **2,500 ENJ** required to create a Nomination Pool. - -“**Pool State**” refers to the status associated with a Nomination Pool. This could be one of the following two: “_Open_” or “_Destroying_” - -## Getting Started - -### Unaltered Behaviors - -The Enjin Blockchain extends upon the implementation of the basic [staking pallet](https://crates.parity.io/pallet_staking/index.html) and [nomination pool pallet](https://wiki.polkadot.network/docs/learn-nomination-pools) to enable functionality such as Liquid Staking. Because it is an extension to an existing set of pallets, there are some queries and extrinsics that remain unaltered. - -1. [Listing All Pools](#listing-all-nomination-pools) - -2. [Listing a Single Pool](#listing-a-single-nomination-pool) - -3. [Listing Unbonding Members of a Pool](#listing-unbonding-members-of-a-nomination-pool) - -4. [Bonding into a Pool](#bonding-into-a-nomination-pool) - -5. [Unbonding from a Pool](#unbonding) - -6. [Withdrawing from a Pool](#withdrawing) - -7. [Nominating Validators for a Pool](#nominating-validators-for-a-nomination-pool) - -8. [Chilling a Pool](#chilling-a-nomination-pool) - -9. [Unbonding a Pool Deposit](#unbonding-1) - -10. [Withdrawing a Pool Deposit](#withdrawing-1) - -11. [Destroying a Pool](#destroying-a-nomination-pool) - -### Enjin Relaychain RPC Endpoints - -It is strongly recommended that you operate your own Enjin Relaychain node. You can read more about this in our documentation: [Running Relaychain Nodes](/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/02-run-relaychain-node.md) - -However, you may also use our official RPC endpoints: - -- `https://rpc.relay.blockchain.enjin.io` - -- `wss://rpc.relay.blockchain.enjin.io` - -Please note that our officially operated RPC endpoints are not archive nodes. This is non-essential to this integration guide unless you intentionally want to look at the historical state. - -### Polkadot.js API Library - -This Integration Guide uses the [@polkadot/api](https://github.com/polkadot-js/api) library, and assumes that the client has been defined in a variable named: api - -### About Degens - -[Degens](https://degens.io/) are a powerful NFT that are the backbone of staking on the Enjin Blockchain. These NFTs are essential to staking as they are a **requirement** for creating a Nomination Pool.  - -Each Degen can be assigned to a single Nomination Pool (this occurs during the Nomination Pool creation process) and then cannot be disassociated from the Nomination Pool unless it is destroyed. A Degen cannot be allocated to multiple Nomination Pools at the same time. Whoever holds the Degen is the “owner” of the Nomination Pool. - -Degens are built on-top of Enjin Blockchain’s incredibly powerful and versatile [multiToken pallet](/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md). - -### About sENJ and Liquid Staking - -sENJ is an . Due to it being a multi-token, this is what enables the concept of Liquid Staking. Liquid Staking, simply put, means that the sENJ is not locked to a specific account and it can in fact be used in processes such as governance or the [Stake Exchange](/04-enjin-blockchain/02-enjin-relaychain/02-stake-exchange-pallet.md). - -#### Difference Between ENJ and sENJ - -ENJ is the native token of the Enjin Blockchain. Whereas, sENJ is an created with the [multiToken pallet](/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md). When [bonding (staking)](/02-guides/02-blockchain/01-relaychain/04-joining-nomination-pools.md) ENJ to a nomination pool, sENJ is created at a 1:1 ratio. During the time that you are in the pool, holding sENJ, a Stake Factor accrues as the nomination pool accrues ENJ. This Stake Factor is a multiplier to the value of sENJ the user holds. This means that when the user then proceeds with unbonding, they in-turn can withdraw up to the amount factoring in the Stake Factor multiplier. - -The Stake Factor can decrease if the nominated Validators misbehave. Therefore, it is not possible to guarantee that the user would definitely receive, at minimum, their original ENJ balance or, at best, an amount greater than their original ENJ balance. - -## Integration Guide - -### Queries - -#### Listing All Nomination Pools - -**This query is unaltered from the original.** This query can be used to retrieve an exhaustive list of all Nomination Pools, along with details about each of the Nomination Pools. - -##### State Storage Query - -```javascript -nominationPools.bondedPools -``` - -##### Example Request - -```javascript -const pools = (await api.query.nominationPools.bondedPools.entries()) - .map(([account, data]) => [account.toHuman(), data]) - -console.log(Object.fromEntries(pools)) -``` - -##### Example Response - -```json -{ - "0": { - "state": "Open", - "commission": { - "current": null, - "max": null, - "changeRate": null, - "throttleFrom": null - }, - "tokenId": 1, - "capacity": "0x00000000001a784379d99db420000000", - "creationBlock": 1654079, - "name": "0x" - }, - ... -} -``` - -#### Listing a Single Nomination Pool - -**This query is unaltered from the original.** This query can be used to retrieve details about a specific Nomination Pool. - -##### State Storage Query - -```javascript -nominationPools.bondedPools(poolId : u32) -``` - -##### Example Request - -```javascript -const pool = await api.query.nominationPools.bondedPools(0) -console.log(pool) -``` - -##### Example Response - -```json -{ - "state": "Open", - "commission": - { - "current": null, - "max": null, - "changeRate": null, - "throttleFrom": null - }, - "tokenId": 1, - "capacity": "0x00000000001a784379d99db420000000", - "creationBlock": 1654079, - "name": "0x" -} -``` - -#### Listing Members of a Nomination Pool - -This query can be used to retrieve an exhaustive list of members who are currently staked into a specific Nomination Pool. - -##### State Storage Query - -```javascript -multiTokens.tokenAccounts( - collectionId : u128, // 1 for sENJ - tokenId : u128, // pool id - account : Option -) -``` - -##### Example Request - -```javascript -const pool_id = 0 -const members = (await api.query.multiTokens.tokenAccounts.entries(1, pool_id)) - .map(([account, data]) => [account.toHuman()[2], data]) - -console.log(Object.fromEntries(members)) -``` - -##### Example Response - -```json -{ - "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu": { - "balance": "0x00000000000007c1c839136681335ac3", - "reservedBalance": 0, - "lockedBalance": 0, - "namedReserves": {}, - "locks": {}, - "approvals": {}, - "isFrozen": false - }, - ... -} -``` - -#### Listing Unbonding Members of a Nomination Pool - -**This query is unaltered from the original.** This query can be used to retrieve an exhaustive list of members who are currently unbonding from a specific pool. - -##### State Storage Query - -```javascript -nominationPools.unbondingMembers( - poolId : u32, - account : Option -) -``` - -##### Example Request - -```javascript -const pool_id = 0 - -const unbondingMembers = (await api.query.nominationPools.unbondingMembers.entries(pool_id)) - .map(([account, data]) => [account.toHuman()[1], data]) - -console.log(Object.fromEntries(unbondingMembers)) -``` - -##### Example Response - -```json -{ - "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu": { - "unbondingEras": { - "249": "0x000000000000021e19e0c9bab2401979", - "260": "0x000000000000043c33c1937564803fb3", - "267": "0x000000000000021e19e0c9bab2400e34" - } - }, - ... -} -``` - -#### Reading a User’s ENJ Balance of a Nomination Pool - -This query can be used to read the _true_ ENJ Balance of an account for a specific Nomination Pool. There is a necessary distinction between reading the value of sENJ and the _true_ ENJ Balance that the user currently holds through the sENJ token. Refer to [Difference Between ENJ and sENJ](#difference-between-enj-and-senj) for more information. - -This query is more complicated as it requires first retrieving the Nomination Pool’s stash account, calculating the Stake Factor, retrieving the user’s sENJ balance in the Nomination Pool, and finally multiply their sENJ balance by the Stake Factor to derive their final balance. - -##### Example Request - -```javascript -import { bnToU8a, stringToU8a, u8aConcat, BN } from '@polkadot/util' - -const pool_id = 0 -const account = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" - -function getPoolStashAccount(api, pool_id) { - const pallet_id = api.consts.nominationPools.palletId.toU8a() - - return api.registry.createType('AccountId32', u8aConcat( - stringToU8a('modl'), - pallet_id, - new Uint8Array([1]), - bnToU8a(new BN(pool_id), { bitLength: 32, isLe: true }), - new Uint8Array(32) - )).toString() -} - -const pool_account = getPoolStashAccount(api, pool_id) -const pool_active_balance = (await api.query.staking.ledger(pool_account)).toJSON().active -const staked_enj_supply = (await api.query.multiTokens.tokens(1, pool_id)).toJSON().supply -const pool_stake_factor = pool_active_balance / staked_enj_supply - -const balance = (await api.query.multiTokens.tokenAccounts(1, pool_id, account)) - .toJSON().balance * pool_stake_factor - -console.log(balance) -``` - -Technically, the Stake Factor also considers the Nomination Pool’s reward account as part of the calculation. However, during each payout the funds are automatically reinvested into the Nomination Pool’s stash account, leaving the reward account empty (barring its existential deposit). - -##### Example Response - -``` -4.266220452892553e+22 -``` - -### Extrinsics - -#### Bonding into a Nomination Pool - -**This extrinsic is unaltered from the original.** This extrinsic can be called for the user to join a Nomination Pool and bond a given amount of ENJ into that Nomination Pool. - -##### Extrinsic - -```javascript -nominationPools.bond( - poolId : u32 (PoolId), - amount : PalletNominationPoolsBondValue (BondValueOf) -) -``` - -##### Example Request - -```javascript -const pool_id = 0 -const signer = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" - -// generate transaction to bond 100 ENJ -const tx = api.tx.nominationPools.bond(pool_id, {"amount": "100000000000000000000"}) - -// or generate transaction to bond all ENJ -// const tx = api.tx.nominationPools.bond(pool_id, {"fill": null}) - -// sign and send transaction -await tx.signAndSend(signer, ({ events = [], status, dispatchError }) => { /* handle */ }) -``` - -#### Unbonding and Withdrawing from a Nomination Pool - -**These extrinsics are unaltered from the original.** These extrinsics are permissionless only when the Pool State is _Destroying_. - -##### Unbonding - -This extrinsic can be used to initiate the unbonding process of a given amount of sENJ back to ENJ from a specific Nomination Pool. If the amount being unbonded is the complete amount of sENJ, the user is said to be leaving the Nomination pool. The unbonding process takes twenty-eight (28) eras. - -###### Extrinsic - -```javascript -nominationPools.unbond( - poolId : u32 (PoolId), - memberAccount : MultiAddress (AccountIdLookupOf), - unbondingPoints : Compact (BalanceOf) -) -``` - -###### Example Request - -```javascript -const pool_id = 0 -const account = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" -const amount = "100000000000000000000" // 100 ENJ - -// generate transaction -const tx = api.tx.nominationPools.unbond(pool_id, account, amount) - -// sign and send transaction -await tx.signAndSend(account, ({ events = [], status, dispatchError }) => { /* handle */ }) -``` - -##### Withdrawing - -This extrinsic can be used to withdraw sENJ that has successfully completed the unbonded period of twenty-eight (28) eras. The user will receive the amount unbonded in the form of ENJ. - -###### Extrinsic - -```javascript -nominationPools.withdrawUnbonded( - poolId : u32 (PoolId), - memberAccount : MultiAddress (AccountIdLookupOf), - numSlashingSpans : u32 -) -``` - -###### Example Request - -```javascript -const pool_id = 0 -const account = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" - -// generate transaction -const tx = api.tx.nominationPools.withdrawUnbonded(pool_id, account, 0) - -// sign and send transaction -await tx.signAndSend(account, ({ events = [], status, dispatchError }) => { /* handle */ }) -``` - -#### Creating a Nomination Pool - -This extrinsic is used to create a new Nomination Pool. The duration must be at least 30 eras, and must not exceed 1,000 eras. - -##### Requirements - -- User must own an Inactive Degen. - -- User must bond an 2,500 ENJ pool deposit. - -##### Extrinsic - -```javascript -nominationPools.create( - tokenId : u128 (TokenIdOf), - deposit : Compact (BalanceOf), - capacity : Compact (BalanceOf), - duration : Compact (EraIndex), - name : Bytes (PoolNameOf) -) -``` - -##### Example Request - -```javascript -const token_id = 1 // id of the Inactive Degen -const deposit = "2500000000000000000" // 2,500 ENJ - Pool Deposit -const capacity = "100000000000000000000000" // 100,000 ENJ -const duration = 100 // 100 eras -const name = "Demo Pool" -const signer = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" - -// generate transaction -const tx = api.tx.nominationPools.create( - token_id, - deposit, - capacity, - duration, - name -) - -// sign and send transaction -await tx.signAndSend(signer, ({ events = [], status, dispatchError }) => { /* handle */ }) -``` - -#### Mutating a Nomination Pool - -This extrinsic is used to update (mutate) an existing Nomination Pool. - -##### Requirements - -- User must be the Pool Owner. - -##### Extrinsic - -```javascript -nominationPools.mutate( - poolId : u32 (PoolId), - mutation : PalletNominationPoolsPoolMutation (PoolMutationOf) -) -``` - -##### Example Request - -```javascript -const pool_id = 0 -const signer = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" - -// generate transaction -const tx = api.tx.nominationPools.mutate( - pool_id, - { - duration: "30", // 30 days (optional) - newCommission: { // (optional) - "SomeMutation": "50000000" // 5% (optional) - }, - maxCommission: "90000000", // 9% (optional) - changeRate: { - maxDelta: "5000000", // 0.5% (optional) - minDelay: "15", // 15 days (optional) - }, - capacity: "100000000000000000000000", // 100,000 ENJ (optional) - name: "Demo Pool" (optional) - } -) - -// sign and send transaction -await tx.signAndSend(signer, ({ events = [], status, dispatchError }) => { /* handle */ }) -``` - -#### Nominating Validators for a Nomination Pool - -**This extrinsic is unaltered from the original.** This extrinsic is used to modify which Validators the Nomination Pool has nominated. It is also used to unchill a Nomination Pool. - -##### Requirements - -- User must be the Pool Owner. - -##### Extrinsic - -```javascript -nominationPools.nominate( - poolId : u32 (PoolId), - validators : Vec (Vec) -) -``` - -##### Example Request - -```javascript -const pool_id = 0 -const signer = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" -const validators = [ - "enG2tPZeAkKCNSxgXnWNDhGf5j83GZboj41ypUENcGZCNDheN", - "enBRFocgo4aPAVtny93ZLaTPX9JpbxrSeBt29uXdLy3AcG8Dx", - // ... -] - -// generate transaction -const tx = api.tx.nominationPools.nominate(pool_id, validators) - -// sign and send transaction -await tx.signAndSend(signer, ({ events = [], status, dispatchError }) => { /* handle */ }) -``` - -#### Chilling a Nomination Pool - -**This extrinsic is unaltered from the original.** This extrinsic is used to chill the Nomination Pool — a chilled Nomination Pool refers to one that does not actively contribute its stake to the Validator selection process. Chilling the Nomination Pool takes effect in the next era. - -##### Requirements - -- User must be the Pool Owner. - -##### Extrinsic - -```javascript -nominationPools.chill(poolId : u32 (PoolId)) -``` - -##### Example Request - -```javascript -const pool_id = 0 -const signer = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" - -// generate transaction -const tx = api.tx.nominationPools.chill(pool_id) - -// sign and send transaction -await tx.signAndSend(signer, ({ events = [], status, dispatchError }) => { /* handle */ }) -``` - -#### Destroying a Nomination Pool - -**This extrinsic is unaltered from the original.** This sets the Pool State to _Destroying_. - -:::danger -This process cannot be reversed! -::: - -##### Requirements - -- User must be the Pool Owner. - -##### Extrinsic - -```javascript -nominationPools.destroy(poolId : u32 (PoolId)) -``` - -##### Example Request - -```javascript -const pool_id = 0 -const signer = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" - -// generate transaction -const tx = api.tx.nominationPools.destroy(pool_id) - -// sign and send transaction -await tx.signAndSend(signer, ({ events = [], status, dispatchError }) => { /* handle */ }) -``` - -#### Unbonding and Withdrawing a Pool Deposit from a Nomination Pool - -**These extrinsics are unaltered from the original.** These extrinsics are permissionless. - -##### Unbonding - -This extrinsic will initiate a twenty-eight (28) era unbonding period. - -###### Requirements - -- The Pool State must be _Destroying_. - -- There must be no remaining Pool Members for the Nomination Pool being destroyed. - -##### Extrinsic - -```javascript -nominationPools.unbondDeposit(poolId : u32 (PoolId)) -``` - -##### Example Request - -```javascript -const pool_id = 0 -const signer = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" - -// generate transaction -const tx = api.tx.nominationPools.unbondDeposit(pool_id) - -// sign and send transaction -await tx.signAndSend(signer, ({ events = [], status, dispatchError }) => { /* handle */ }) -``` - -##### Withdrawing - -This extrinsic will withdraw the Pool Deposit which will subsequently destroy the Nomination Pool. The Degen assigned to the Nomination Pool will become an Inactive Degen and will **not** be destroyed alongside the Nomination Pool. - -###### Requirements - -- The Pool Deposit unbonding period (twenty-eight (28) eras)  must have passed. - -###### Extrinsic - -```javascript -nominationPools.withdrawDeposit(poolId : u32 (PoolId)) -``` - -###### Example Request - -```javascript -const pool_id = 0 -const signer = "enCpUHhm4Nk5jbaGkgZNxgNxfEaHUe39ZcfjVEVwYipn1KBLu" - -// generate transaction -const tx = api.tx.nominationPools.withdrawDeposit(pool_id) - -// sign and send transaction -await tx.signAndSend(signer, ({ events = [], status, dispatchError }) => { /* handle */ }) -``` - -## Data Indexing - -It is recommended to index data pertaining to Staking and Nomination Pools as this enables a more performant and richer experience. However, it is **not essential** for a basic integration, as illustrated through this Integration Guide. - -Indexing this data enables you to monitor the change over time and infer the increase or decrease in sENJ holdings over time. This enables your integration to visually show payouts and their associated amounts per Pool Member. - -## Extra Resources / Further Reading - -- [Subscan](https://www.subscan.io/), a Blockchain Explorer, has useful pages on [Validators](https://enjin.subscan.io/validator) and [Nomination Pools](https://enjin.subscan.io/nomination_pool). - -- [Enjin Console](https://console.enjin.io/) has a dedicated page for [Staking](https://console.enjin.io/?rpc=wss%3A%2F%2Frpc.relay.blockchain.enjin.io#/staking) and [Nomination Pools](https://console.enjin.io/?rpc=wss%3A%2F%2Frpc.relay.blockchain.enjin.io#/staking/pools). - -- For more in-depth information, feel free to explore other topics within the [Enjin Relaychain](/04-enjin-blockchain/02-enjin-relaychain/02-enjin-relaychain.md) section. diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/03-creating-nomination-pools.md b/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/03-creating-nomination-pools.md deleted file mode 100644 index 03b7106..0000000 --- a/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/03-creating-nomination-pools.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: "Creating Nomination Pools" -slug: "creating-nomination-pools" ---- -## Creating nomination pools - -Enjin Relaychain uses Nominated Proof of Stake (NPoS) to secure the network. Nominators can select which validators to delegate the stake to. A nomination pool acts as a single nominator and lowers the entry barrier for the user to participate in NPoS. - -## Degen NFT - -To create a nomination pool, an account must hold a Degen NFT. - -A Degen NFT is a special token that is used to create the pool and the holder might receive pool commissions, the collection for all Degen NFTs has a fixed ID of `2`. - -![](/img/components/enjin-relaychain/9.png) - -## Create a nomination pool (RAW extrinsic) - -Once you hold a Degen NFT, you can create a nomination pool using `nominationPools.create()`. - -![](/img/components/enjin-relaychain/10.png) - -## Create a nomination pool (Enjin Console) - -[Enjin Console](https://console.enjin.io/) has custom UI that simplifies this process, its located under `Network -> Staking -> Pools` - -![](/img/components/enjin-relaychain/11.png) - -This page allows to create a nomination pool, see an overview, and manage its settings. Let's create a new pool and fill out the form. - -Important to keep in mind: - -- Creating a pool requires a Degen NFT (`tokenId` must be provided) -- Minimum initial stake deposit is required to create a pool (`2500 ENJ`) -- `Duration` must be at least `30 eras` (30 days) and not more than `1000 eras` (1000 days) - -## Creation parameters - -It's worth noting that `tokenId` is the ID of the Degen NFT you hold. `PoolId` may or may not match the `tokenId`, this wont change anything, but is worth keeping in mind. `Duration` is defined in `eras`, where `1 era = 24 hours` on Enjin Relaychain, and minimum value is set at `30 eras`. `Capacity` is the maximum amount of [sENJ](/04-enjin-blockchain/02-enjin-relaychain/01-validator-staking.md#tokens-overview) that can be staked in the pool. The maximum Capacity is determined by the Degen's token `max_pool_capacity` attribute. -Note that this is sENJ, not ENJ. To learn more about the difference, [check out the Validator Staking page](/04-enjin-blockchain/02-enjin-relaychain/01-validator-staking.md). - -![](/img/components/enjin-relaychain/12.png) - -## Pool created - -Now the nomination pool is created and the initial stake is deposited. You can also notice that the only member is the Pool stash account. - -![](/img/components/enjin-relaychain/13.png) - -## Join a nomination pool - -Now you can also join the pool, and provide your own stake. - -![](/img/components/enjin-relaychain/14.png) - -## Pool overview - -Once you have joined the pool, you can check the overview of the pool, and the members list should be updated. - -The pool is currently not nominating any validators and is not earning any rewards. - -![](/img/components/enjin-relaychain/15.png) - -## Nominate validators - -This step is very crucial to the whole operation. The rewards will start to accrue when the pool is nominating validators that are selected to produce blocks. To select nominators, use the `Nominate` button that is located under each expanded pool. - -![](/img/components/enjin-relaychain/16.png) - -At this point we have nominated both validators and should just wait to see the rewards. Once some rewards have accrued and an era has passed, the `Payout rewards` button can be used to distribute rewards to everyone in the pool. Rewards are paid out once per validator per era. - -![](/img/components/enjin-relaychain/17.png) - -## Mutating nomination pool - -As the pool owner, you can change the settings of the pool. This includes changing the duration, commission, and management roles of the pool. To do this, use the `Mutate` button that is located under each expanded pool. - -![](/img/components/enjin-relaychain/18.png) - -## Destroying nomination pool - -Destroying a nomination pool involves a series of steps. - -:::tip -For a more convenient way to destroy a pool, consider using [NFT.io](https://nft.io). -::: - -To manually destroy a pool using the Enjin Console, follow these steps: -1. **Change the Pool State to Destroying**: Initiate the destruction process by setting the pool's state to `Destroying`. You can do this by calling the `nominationPools.destroy()` extrinsic. -2. **Unbond Member Stakes**: If any members have staked funds in the pool, their stakes must be unbonded. Once the pool is in the `Destroying` state, the `nominationPools.unbond()` extrinsic becomes permissionless. This means the pool owner can initiate unbonding for all members by calling `nominationPools.unbond()` on their behalf. After the unbonding period for all members ends, their stakes must be withdrawn using the `nominationPools.withdrawUnbonded()` extrinsic, which is also permissionless at this stage. -3. **Unbond and Withdraw the Initial Deposit**: Once the pool is in the `Destroying` state and has no bonded members, the pool owner must unbond their initial ENJ stake deposit. Do this by calling the `nominationPools.unbondDeposit()` extrinsic. After the unbonding period for the deposit ends, use the `nominationPools.withdrawDeposit()` extrinsic to withdraw it and finalize the pool's destruction. \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/04-joining-nomination-pools.md b/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/04-joining-nomination-pools.md deleted file mode 100644 index 41c8557..0000000 --- a/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/04-joining-nomination-pools.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: "Joining Nomination Pools" -slug: "joining-nomination-pools" ---- -:::info The Enjin Blockchain Console -Use [console.enjin.io](https://console.enjin.io/) to use the user interface referenced in this document. -::: - -:::tip -For a more user friendly staking interface, we suggest to use the [Enjin Wallet](https://enjin.io/products/wallet). -::: - -Enjin Relaychain operates using Nominated Proof of Stake (NPoS) consensus algorithm. NPoS is a variation of Proof of Stake (PoS) consensus algorithm, where the right to produce blocks is delegated to a set of validators. Validators are selected by nominators, who delegate their stake to validators. Validators are rewarded for producing blocks and are sharing the rewards with their nominators. - -## Find a nomination pool that satisfies your needs - -![](/img/components/enjin-relaychain/20.png) - -You should look out for the following criteria when choosing a nomination pool: - -- **Points -** Points display the amount of staked ENJ (sENJ) this pool has in circulation. Its important to remember that each pool issues its own unique `sENJ-poolID` token, so the amount of points does not directly correlate to the amount of ENJ staked in the pool. -- **Capacity -** shows the maximum amount of sENJ that the pool can issue. If the capacity is reached, you will not be able to stake ENJ in the pool. -- **Nominees -** displays the validators that the pool is nominating. Remember that the pool will only receive rewards if their nominated validators are selected to produce a block. If the validator is slashed for any reason (e.g. being offline for too long), the funds nominated by pool will be slashed as well. -- **Members -** shows the current participants for each nomination pool. More members can provide higher stake, but it also means that rewards will be split between more people. - -![](/img/components/enjin-relaychain/21.png) - -## Stake ENJ in a nomination pool - -The pool with `ID 0` is nominating `Alice_STASH` validator and has 3 pool members (including the pool account). To stake 100 ENJ in this pool, click on the `Join` button. - -![](/img/components/enjin-relaychain/22.png) - -Let's say Dave wishes to join this pool with 100 ENJ. With current staked ENJ and issued sENJ, he should receive 100 points (sENJ) for his stake. - -![](/img/components/enjin-relaychain/23.png) - -Now we can also check our `sENJ-poolID` balance to be sure our stake was successful: - -![](/img/components/enjin-relaychain/24.png) - -This will return Dave's `sENJ-poolID` balance for this specific pool with ID 0, which is also the sENJ token id. And this returns a balance of 100 sENJ units. - -![](/img/components/enjin-relaychain/25.png) - -### Rewards - -Rewards accrue every era (24h) for validators and subsequently to nominators. They can be claimed using n`ominationPools.payoutRewards()` extrinsic, which is easily accessible under each pool when expanded. This can be called by any member of the pool and will distribute rewards to all members of the pool including validators. - -![](/img/components/enjin-relaychain/26.png) - -### Inflation - -Every era the rewards are generated for NPoS participants, creating inflation. The inflation rate is fixed at `5% per year`. So every era, `~0.0134%` of the total ENJ supply on chain is generated and distributed to NPoS participants. Calculation takes compounding effect into consideration. The rewards are distributed proportionally to the amount of stake each participant has in the network. - -![](/img/components/enjin-relaychain/27.png) - -## Leaving a nomination pool - -To leave a nomination pool, expand the pool details and click `Unbond`. -:::note -You must wait 28 eras for the unbonding period to complete. Once finished, click `Withdraw unbonded` to retrieve your funds. -::: - -![](/img/components/enjin-relaychain/28.png) - -We can see that correct events were emitted, which also indicates that rewards were paid out to all members of the pool and now the same 100 sENJ are worth more than 100 ENJ, and Dave unbonds `100.608836621592543849 ENJ`: - -![](/img/components/enjin-relaychain/29.png) - -Unbonded funds can only be withdrawn once the correct era is reached. We can observe how much and when each unbonding member can withdraw by looking at the chain state - -![](/img/components/enjin-relaychain/30.png) - -This informs us that at era 118, we will be able to withdraw our unbonded ENJ from the pool (including reward). We can see current era at `staking.currentEra` storage. - -![](/img/components/enjin-relaychain/31.png) - -And now that we are past the unbonding era, we can withdraw our funds: - -![](/img/components/enjin-relaychain/32.png) - -And we can see that the correct amount of ENJ was withdrawn from the pool and transferred to us (Dave): - -![](/img/components/enjin-relaychain/33.png) diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/_category_.json b/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/_category_.json deleted file mode 100644 index 6d49583..0000000 --- a/versioned_docs/version-2.x/02-guides/02-blockchain/01-relaychain/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Relaychain", - "link": { - "type": "generated-index", - "description": "Explore guides on integrating staking, creating, and joining nomination pools on the Enjin Relay Chain." - } -} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-enjin-blockchain-nodes.md b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-enjin-blockchain-nodes.md deleted file mode 100644 index b4865bd..0000000 --- a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-enjin-blockchain-nodes.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: "Enjin Blockchain Nodes" -slug: "../about-enjin-blockchain-nodes" ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; - -## Types of Nodes - -Enjin supports three types of nodes, RPC, collator, and archive. Choosing the type of node that best suits your use case is crucial since each type has specific requirements and uses. The good news is that you only need to compile the node once, and then you can run it as an RPC node, collator, or archive node. - -### RPC Node - -An RPC node is a type of node that can connect to the Enjin network and participate in it. Typically, it is hosted on a remote server and accessed by client-side applications as an RPC or WS endpoint, serving as a connection interface to the Matrixchain. However, RPC nodes do not store the entire history of the chain, meaning they cannot provide historical data. It is important to consider this limitation when choosing an RPC node for your use case. - -### Collator Node - -A collator node is responsible for maintaining a Matrixchain by collecting transactions from users and producing state transition proofs for Relaychain validators. Collators also maintain a full node for the Relaychain and a full node for their particular Matrixchain. This means they retain all the necessary information to author new blocks and execute transactions the same way as validators do on current PoS blockchains. Typically, collators will collate and execute transactions to create a block, then broadcast it to other collators and Relaychain validators on the network. - -:::warning -Currently, the functionality of running a collator node and the associated rewards system is not available. We will be sure to provide updates as soon as these features become available. -::: - -### Archive Node - -An archive node stores the complete history of the network and is typically used by developers for testing applications and running analytics. It is also used by blockchain explorers to enable users to explore the entire state of the chain, as well as by collators to verify the network's state. However, running an archive node requires significantly more disk space compared to other node types. - -## Network Participation - -Users can earn rewards by running a collator node to participate in the Enjin network. However, it's essential to understand that collators do not secure the network. In Enjin, PoA (Proof of Authority) consensus is used, and block producers or collators are chosen from the list of authorized validators. More collators do not necessarily mean a better or more secure network since Relaychain validators will reject an invalid Matrixchain block. -Having too many collators can also slow down the network. The only power that collators possess is transaction censorship. To avoid censorship, a Matrixchain must have some neutral collators, but not necessarily a majority. In theory, the censorship problem can be solved by having only one honest collator. - -## Staking - -The management of collators on the Enjin Matrixchain is handled by the collator staking pallet. This pallet selects and rewards block producers for each round. Two types of rewards are generated for every block created, which are collected from transaction tips and fees. These fees are then transferred to the fee distribution account at the end of every block. The accumulated fees are then distributed at the end of every session by the Pools pallet to registered pools. Currently, the collator receives a 70% share from the fee distribution. The collator staking pallet then distributes these rewards to collators at the end of each session using the funds received in the collator pool account. - -## Rewards - -The formula for calculating the block reward is: -reward = `blocks_authored_by_collator \* reward_per_block` -where `reward_per_block = total_reward_per_session / total_blocks_in_session` -The rewards are then distributed depending on the type of collator: - -- Invulnerable Collators: the fees are transferred back to the fee distribution account -- Other Collators: fees are transferred to collator accounts - -## Collator selection - -Collators are selected for each session based on the following criteria: - -- Invulnerable nodes are always selected as collators -- The remaining number of collators (`MaxCollators - Invulnerables`) is filled with the top candidates ordered by total stake (Collator's own funds and nominators' funds). - -Invulnerable nodes can only be set by governance or . diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-running-a-node.md b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-running-a-node.md deleted file mode 100644 index 4f86345..0000000 --- a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-running-a-node.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "Hosting an Enjin Node" -slug: "running-a-node" -description: "Running an Enjin node allows you to connect to the Enjin network, expose the RPC endpoint, and author blocks. This guide walks you through the process of running your own node locally and on a remote server." ---- -## Requirements - -In general, requirements for running an Enjin node are quite similar to running a Substrate node. - -Therefore, if you are not sure about specific details, you can always refer to the [official Substrate documentation](https://substrate.dev/docs/en/knowledgebase/getting-started/). However, building a Matrixchain Node, i.e. Enjin Matrixchain node, requires a little more time and resources, since Matrixchain nodes also run a full Relaychain node. - -### Hardware - -For building and running an Enjin node, the recommended hardware is: - -| Component | Requirement | -| :--------- | :-------------------- | -| OS | Ubuntu 20.04 or newer | -| CPU | 4 cores | -| RAM | 16 GB | -| Disk Space | 256 GB SSD | - -:::tip -It is recommended to have more disk space available (512 GB+) if you are running an Archive Node. -::: - -### Ports - -This table shows the default ports that are used by the Enjin Matrixchain node and embedded Relaychain node: - -| Description | Ingress Port | Egress Port | -| :-------------- | :------------------- | :------------------------ | -| P2P | **TCP** 30333, 30334 | **TCP** 443, 30333, 30334 | -| RPC (HTTP & WS) | **TCP** 9933 | - | -| Prometheus | **TCP** 9615 | - | - -This table shows the ports that are recommended to be exposed depending on the type of node you are running: - -| Description | Archive / RPC | Collator / Validator | -| :-------------- | :------------ | :------------------- | -| P2P | **required** | **required** | -| RPC (HTTP & WS) | ✅ | ❌ | -| Prometheus | ✅ | ✅ | - -## Advanced - -This section covers advanced topics associated with running the Enjin node. - -### Node Key - -Substrate nodes use node keys for identification in peer-to-peer communication. To generate a node key, use the [subkey tool](https://docs.substrate.io/reference/command-line-tools/subkey/), which can be downloaded using the following command: - -`cargo install --force --git https://github.com/paritytech/substrate subkey` - -After installing the [subkey tool](https://docs.substrate.io/reference/command-line-tools/subkey/), you can generate the node key using the following command: - -`subkey generate-node-key --file ./node.key # or any other path` - -### Purging the chain - -To begin anew, without any chain history, you may want to purge the chain. - -:::danger -The following will command will purge the chain data for the Enjin Matrixchain -::: - -`./target/release/matrixchain purge-chain --chain mainnet` diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/02-run-relaychain-node.md b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/02-run-relaychain-node.md deleted file mode 100644 index 067d4bd..0000000 --- a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/02-run-relaychain-node.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -title: "Run a Relaychain Node" -slug: "run-relaychain-node" ---- -There are two different ways in which you can a Node, the first one being through Docker, and the second one through a Binary (either precompiled or [Built from Source](/enjin-blockchain-nodes/building-from-source)). - -## Create Data Directory - -First begin by creating a data directory for the: `sudo mkdir /data` - -Then set the permission for it using `sudo chown -R 1000:1000 /data` - -## Docker - -:::info -The Docker Image can be found on Docker Hub at: [enjin/relaychain](https://hub.docker.com/r/enjin/relaychain). -In this example, we demonstrate using the version `latest`. However, in practice, we recommend statically setting this to a specific version (such as `v100`) and then performing manual upgrades to your nodes as and when appropriate. This is to ensure that your node doesn't inadvertently differ from one restart to the next. See the [Upgrading using Docker](#upgrading-using-docker) section for more information. -::: - -You can use the following `docker-compose.yml` file: - -```yaml -services: - relaychain: - container_name: relaychain - image: enjin/relaychain:latest - ports: - - 9944:9944 - - 9615:9615 - - 30333:30333 - - 30334:30334 - volumes: - - /data:/chainstate - command: [ - "--name=enjin-relay-docker", - "--rpc-external", - "--rpc-cors=all", - "--chain=enjin", - "--base-path=/chainstate" - ] -``` - -Simply run the command `docker-compose up -d` to run the container. - -You can use the command `docker ps` to check that the container is running. You can then stream the logs (by taking the ``) to check its current sync position by using: `docker logs -f --since 1m` - -## Binary - -### Command - -`$ ./enjin --name "enjin-relay-docker" --rpc-external --chain enjin` - -Connecting to your node - -Depending on the use case, there are a couple of ways to connect to the node: - -- WebSocket Connection: `ws://localhost:9944` -- RPC (HTTP) Connection: `http://localhost:9944` - -## Archive Node - -In order to run an archive node, the following argument needs to be passed to either the binary or added to the command section of the `docker-compose.yml` file: - -`--pruning=archive` - -This should be appended after line 19 in the `docker-compose.yml` file, or at the end of the Binary execution command. - -## Upgrading - -:::info It is not always possible to downgrade a node. -In the event the node incurred a database upgrade, it is no longer possible to downgrade the node to an older version without first re-syncing the node. - -Additionally, in the event an on-chain upgrade has been enacted that requires a newer node minimum version, it will not be possible to downgrade below that version as new blocks following the upgrade will fail to import. -**Always keep your node version up-to-date.** -::: - -In order to ensure compatibility with the chain at all times, and to ensure the best security, it is imperative that node operators keep their nodes up-to-date with each release that is published. The process of upgrading is incredibly simple, and in almost all cases, requires very little involvement by the node operator. This is because, the node will automatically detect when it's using an older version of the database and automatically upgrade it to the latest one that is compatible with the - -We recommend that node operators subscribe to our mailing list [mailing-list-node-operators@enjin.io](https://groups.google.com/a/enjin.io/g/mailing-list-node-operators) in order to be informed when we publish a new node version. However, for those who don't want to subscribe, you can query our [Docker Hub repository](https://hub.docker.com/r/enjin/relaychain/tags) to check for new versions. All versions are automatically pushed to Docker Hub, so you will always be able to find the latest version there. - -### Upgrading using Docker - -Open your `docker-compose.yml` in a text editor and locate the `image` line, in the below example it is line 4: - -```yaml -services: - relaychain: - container_name: relaychain - image: enjin/relaychain:latest # <-- this line - ports: - - ... -``` - -If you are using the version `latest` (as in the above example), simply restarting that node at any point will ensure you're automatically updated to the very latest stable version. However, if you have statically set an image version, simply alter the line to specify the latest versioned release that appears on [Docker Hub](https://hub.docker.com/r/enjin/relaychain/tags). - -For example, if you are running `enjin/relaychain:v100` and the latest version is `enjin/relaychain:v200` you would simply need to make the following change: - -```yaml -services: - relaychain: - container_name: relaychain -+ image: enjin/relaychain:v200 # add this line -- image: enjin/relaychain:v100 # remove this line - ports: - - ... -``` - -Once updated, simply restart the node by first stopping the existing node and then repeating the steps to start the node using the command `docker-compose up -d` in the directory where the `docker-compose.yml`file is located. - -### Upgrading using Binary - -Simply download the latest binary and run it as you have always done as per the [Binary, Command](/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/02-run-relaychain-node.md#command) section. It's as simple as that! - -#### Upgrading from Source - -If you are upgrading based on a build you've produced from the source code, you must navigate to the directory containing the source code in a terminal. Once you have built the binary, simply follow the instruction [Upgrading using Binary](/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/02-run-relaychain-node.md#upgrading-using-binary) to complete the upgrade. - -##### Git-cloned Repository - -You should then fetch the latest changes using `git fetch` and then retrieve the source code of the latest tagged version `git checkout `. Once you have swapped to the latest source code for that release, you can simply run `cargo build --release` (as guided in [Building From Source](/enjin-blockchain-nodes/building-from-source)) to produce a new binary using the latest version. - -##### Downloaded Repository - -In the event that the repository was directly downloaded, and not cloned via `git`, you will first need to download the source code for the latest version. Once done, extract the source code and navigate to the directory in a terminal. You can then run `cargo build --release` (as guided in [Building From Source](/enjin-blockchain-nodes/building-from-source)) to produce a new binary. - -## Looking to run a validator? - -See the [Running a Validator](/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md) section. diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/03-run-matrixchain-node.md b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/03-run-matrixchain-node.md deleted file mode 100644 index c4dc4a5..0000000 --- a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/03-run-matrixchain-node.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -title: "Run a Matrixchain Node" -slug: "run-matrixchain-node" ---- -There are two different ways in which you can a Node, the first one being through Docker, and the second one through a Binary (either precompiled or [Built from Source](/enjin-blockchain-nodes/building-from-source)). - -:::warning Currently, it is not possible to run a Enjin Matrixchain collator node. -This guide will be updated once that option is available. -::: - -## Docker - -:::info -The Docker Image can be found on Docker Hub at: [enjin/matrixchain](https://hub.docker.com/r/enjin/matrixchain) -In this example, we demonstrate using the version `latest`. However, in practice, we recommend statically setting this to a specific version (such as `v100`) and then performing manual upgrades to your nodes as and when appropriate. This is to ensure that your node doesn't inadvertently differ from one restart to the next. See the [Upgrading using Docker](#upgrading-using-docker) section for more information. -::: - -You can use the following `docker-compose.yml` file: - -```yaml -services: - matrixchain: - container_name: matrxichain - image: enjin/matrixchain:latest - ports: - - 9944:9944 - - 9615:9615 - - 30333:30333 - - 30334:30334 - volumes: - - /MY/LOCAL/DIRECTORY:/chainstate - command: [ - "--name=enjin-matrix-docker", - "--rpc-external", - "--rpc-cors=all", - "--chain=mainnet", - "--base-path=/chainstate", - "--", - "--name=enjin-matrix-docker", - "--port=30334", - "--chain=enjin", - "--base-path=/chainstate" - ] -``` - -Simply run the command `docker-compose up -d` to run the container. - -## Binary - -### Command - -```bash -$ ./matrix --name "enjin-matrix-docker" --rpc-external --chain mainnet - -- --name="enjin-matrix-docker" --port 30334 --chain enjin -``` - -### Connecting to your node - -Depending on the use case, there are a couple of ways to connect to the node: - -- WebSocket Connection: `ws://localhost:9944` -- HTTP Connection: `http://localhost:9944` - -## Archive Node - -In order to run an archive node, the following argument needs to be passed to either the binary or added to the command section of the `docker-compose.yml` file: - -`--pruning archive` - -These two arguments should be appended after line 19 in the `docker-compose.yml` file, or just before the backslash (`\`) on line 1 (end of line) of the Binary execution command. - -## Upgrading - -:::warning It is not always possible to downgrade a node. -In the event the node incurred a database upgrade, it is no longer possible to downgrade the node to an older version without first re-syncing the node. - -Additionally, in the event an on-chain upgrade has been enacted that requires a newer node minimum version, it will not be possible to downgrade below that version as new blocks following the upgrade will fail to import. -**Always keep your node version up-to-date.** -::: - -In order to ensure compatibility with the chain at all times, and to ensure the best security, it is imperative that node operators keep their nodes up-to-date with each release that is published. The process of upgrading is incredibly simple, and in almost all cases, requires very little involvement by the node operator. This is because, the node will automatically detect when it's using an older version of the database and automatically upgrade it to the latest one that is compatible with the - -We recommend that node operators subscribe to our mailing list [mailing-list-node-operators@enjin.io](https://groups.google.com/a/enjin.io/g/mailing-list-node-operators) in order to be informed when we publish a new node version. However, for those who don't want to subscribe, you can query our [Docker Hub repository](https://hub.docker.com/r/enjin/matrixchain/tags) to check for new versions. All versions are automatically pushed to Docker Hub, so you will always be able to find the latest version there. - -### Upgrading using Docker - -Open your `docker-compose.yml` in a text editor and locate the `image` line, in the below example it is line 4: - -```yaml -services: - matrixchain: - container_name: matrixchain - image: enjin/matrixchain:latest # <-- this line - ports: - - ... -``` - -If you are using the version `latest` (as in the above example), simply restarting that node at any point will ensure you're automatically updated to the very latest stable version. However, if you have statically set an image version, simply alter the line to specify the latest versioned release that appears on [Docker Hub](https://hub.docker.com/r/enjin/matrixchain/tags). - -For example, if you are running `enjin/matrixchain:v100` and the latest version is `enjin/matrixchain:v200` you would simply need to make the following change: - -```yaml -services: - matrixchain: - container_name: matrixchain -+ image: enjin/matrixchain:v200 # add this line -- image: enjin/matrixchain:v100 # remove this line - ports: - - ... -``` - -Once updated, simply restart the node by first stopping the existing node and then repeating the steps to start the node using the command `docker-compose up -d` in the directory where the `docker-compose.yml`file is located. - -### Upgrading using Binary - -Simply download the latest binary and run it as you have always done as per the [Binary, Command](#command) section. It's as simple as that! - -#### Upgrading from Source - -If you are upgrading based on a build you've produced from the source code, you must navigate to the directory containing the source code in a terminal. Once you have built the binary, simply follow the instruction [Upgrading using Binary](#upgrading-using-binary) to complete the upgrade. - -##### Git-cloned Repository - -You should then fetch the latest changes using `git fetch` and then retrieve the source code of the latest tagged version `git checkout `. Once you have swapped to the latest source code for that release, you can simply run `cargo build --release` (as guided in [Building From Source](/enjin-blockchain-nodes/building-from-source)) to produce a new binary using the latest version. - -##### Downloaded Repository - -In the event that the repository was directly downloaded, and not cloned via `git`, you will first need to download the source code for the latest version. Once done, extract the source code and navigate to the directory in a terminal. You can then run `cargo build --release` (as guided in [Building From Source](/enjin-blockchain-nodes/building-from-source)) to produce a new binary. diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/_category_.json b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/_category_.json deleted file mode 100644 index 5d8916e..0000000 --- a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Enjin Blockchain Nodes" -} - \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md deleted file mode 100644 index 210d0b7..0000000 --- a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md +++ /dev/null @@ -1,210 +0,0 @@ ---- -title: "Running a Validator" -slug: "run-a-validator" ---- -## Preliminaries​ - -Operating a Validator carries significant responsibility. You're responsible for both your stake and that of your nominators. Errors can lead to a loss of tokens and harm your reputation. Despite this, being a Validator can be fulfilling as you help secure a decentralized network and increase your holdings. - -:::warning -You should have strong system administration skills before trying to run your own Validator. You'll need to address technical problems with your node independently. Being a Validator is more than just running the Enjin Relaychain binary. -::: - -### How much ENJ do I need to become an Active Validator? - -There's a distinction between becoming a Validator, and becoming an Active Validator. In order to become a Validator, you need to bond a minimum of 15,000 ENJ. However, becoming an Active Validator requires further economical backing. There is currently a maximum of 13 Active Validators, and so your total stake must be within the top 13. The total stake of a Validator consists of the Validator's own stake, along with the collective stake of all nominators that nominate it. Outside of the top 13, your Validator will remain waiting. - -The simplest way to estimate the required stake would be to navigate to the `Staking` \> `Targets` page on the Enjin Console and read the value of "Lowest Staked" and staking an additional 1 ENJ on-top of that value. - -:::note -The minimum backing required to become an Active Validator on Enjin Relaychain, as of era 659, is 32.5 million ENJ. -::: - -:::danger Operating an insecure or improper setup may result in loss of ENJ! -It is important to acknowledge that any ENJ that you stake for your Validator is liable to being slashed. If you are not confident in your ability to run a Validator, it is recommended to nominate your ENJ to a trusted Validator instead. -::: - -## Initial Set-up - -### Requirements - -To run a Validator, most people use a cloud server with Linux. It's recommended to use a recent version of Debian Linux. This guide uses `Ubuntu 22.04 LTS`, but steps should be similar for other operating systems. - -### Reference Hardware - -Enjin Relaychain's transaction weights are based on specific hardware benchmarks. These were tested on Amazon's `m6id.xlarge`. Validators should use hardware that matches or exceeds these benchmarks to process blocks efficiently. - -- **CPU** - - x86_64 architecture - - 4-cores @ 3.4 GHz - - Intel Ice Lake (or newer); or AMD Zen3 (or newer) -- **Storage** - - 192 GB NVMe SSD: it is important to note that, overtime, the size of the Enjin Blockchain, and its ecosystem, will grow and therefore scaling of the storage is imperative. -- **Memory** - - 16 GB DDR4 ECC - -:::tip -It is not necessary to use an excessive number of CPU cores as operating a node is a single-threaded operation. Therefore, using a higher clocked CPU with 4-cores is more than sufficient. -::: - -The above hardware specifications are not the minimum specifications. However, they should be considered as best practice. Operating a Validator requires the use of professional hardware. Using inferior hardware can lead to performance problems, fewer era points, and even potential slashing. - -### Install a Network Time Protocol (NTP) Client - -[NTP](https://en.wikipedia.org/wiki/Network_Time_Protocol) is a networking protocol designed to synchronize the clocks of computers over a network. NTP allows you to synchronize the clocks of all the systems within the network. Currently it is required that the local clocks of Validators stay reasonably in-sync. Therefore, you should be running a NTP. Most modern Linux installations include an NTP client installed out-of-the-box for time synchronisation. - -#### Checking System Clock Synchronization - -You can check whether you currently have a NTP client installed by running the following command: - -`timedatectl` - -In the event that you have a NTP client installed, and is running, you should see a message similar to the following being output upon running the above command: - -`System clock synchronized: yes` - -#### Install an NTP Client - -If you do not see the message, you can install a NTP client using the following command: - -`sudo apt-get install ntp` - -Once installed, `ntpd` (the NTP client daemon) will start automatically. You can then query to ensure that system clock has synchronized by repeating the steps under [Checking System Clock Synchronization](/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md#checking-system-clock-synchronization). - -:::danger -If you skip this step, your Validator may miss chances to author blocks. If the clock isn't accurate, the network may not accept the blocks you produce. This will result in ImOnline heartbeats making it on chain, but zero allocated blocks making it on chain. -::: - -### Running a Relaychain Node - -You can refer to the page on [Running a Relaychain Node](/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/02-run-relaychain-node.md) in order to get setup. - -#### Synchronize Chain State - -In order to validate, you must first have a node that has fully synchronized. This is simply a matter of running your node and allowing it sufficient time to complete its synchronization. It may take several hours to complete this process. - -The syncing process can be identified as a result of lines been written to your console indicating "Syncing," like so: - -```text -2023-10-10 19:39:00 ⚙️ Syncing, target=#1861415 (8 peers), best: #1733 (0xf5eb…68ab), finalized #1536 (0x14c5…d421), ⬇ 1.0MiB/s ⬆ 15.5kiB/s -2023-10-10 19:39:05 ⚙️ Syncing 439.6 bps, target=#1861415 (8 peers), best: #3931 (0xa854…ea3c), finalized #3584 (0xc223…cab1), ⬇ 580.4kiB/s ⬆ 2.7kiB/s -2023-10-10 19:39:10 ⚙️ Syncing 428.6 bps, target=#1861416 (8 peers), best: #6074 (0x7a75…e2db), finalized #5632 (0x4944…9413), ⬇ 579.2kiB/s ⬆ 1.7kiB/s -2023-10-10 19:39:15 ⚙️ Syncing 417.2 bps, target=#1861417 (8 peers), best: #8160 (0xbb52…b682), finalized #7680 (0x61ce…91bc), ⬇ 577.6kiB/s ⬆ 1.4kiB/s -2023-10-10 19:39:20 ⚙️ Syncing 416.2 bps, target=#1861418 (8 peers), best: #10241 (0x3303…f6a6), finalized #10240 (0xaac3…9f1c), ⬇ 582.3kiB/s ⬆ 4.0kiB/s -``` - -You will know when it has synced, as you will no longer see lines as illustrated above and instead you'll see lines indicating blocks have been "Imported": - -```text -2023-10-18 14:33:24 ✨ Imported #1971895 (0xa37a…4ec9) -2023-10-18 14:33:24 ✨ Imported #1971895 (0xa37a…4ec9) -2023-10-18 14:33:22 💤 Idle (10 peers), best: #1971894 (0xe81a…d878), finalized #1971891 (0xa9fc…7deb), ⬇ 4.9kiB/s ⬆ 9.1kiB/s -2023-10-18 14:33:19 💤 Idle (9 peers), best: #1971894 (0xe81a…d878), finalized #1971890 (0x7065…145e), ⬇ 5.2kiB/s ⬆ 3.9kiB/s -2023-10-18 14:33:18 ✨ Imported #1971894 (0xe81a…d878) -2023-10-18 14:33:18 ✨ Imported #1971894 (0xe81a…d878) -2023-10-18 14:33:17 💤 Idle (10 peers), best: #1971893 (0xb933…472d), finalized #1971890 (0x7065…145e), ⬇ 7.1kiB/s ⬆ 6.1kiB/s -2023-10-18 14:33:14 💤 Idle (9 peers), best: #1971893 (0xb933…472d), finalized #1971890 (0x7065…145e), ⬇ 8.6kiB/s ⬆ 10.7kiB/s -``` - -#### Session Keys - -If you're uncertain about your node's current session keys after the setKeys transaction, you can check using two RPC methods: hasKey for a specific key or hasSessionKeys for the entire session key string. - -Once your node has been fully synced, it's now time to generate your session keys. You need to stop the node and run it again using the `--validator` option. Running with the `--validator` option also requires you to specify the `--public-addr` option. You can do this by first acquiring your public IP address (ex. `198.51.100.69`) and then passing: `--public-addr /ip4/198.51.100.69/tcp/30334/ws` - -`$ ./enjin --validator --rpc-methods=unsafe --name "a_reasonably_unique_name_a8f310" --public-addr /ip4/[YOUR-IP]/tcp/30334/ws` - -You will know if this was successful as you will see an output during startup indicating your role is now `AUTHORITY` and an indication of the `BABE` authorship worker being started. This can be observed in the below example: - -```text -2025-08-15 08:34:07 Enjin -2025-08-15 08:34:07 ✌️ version 1.6.0-2dafb82dcb4 -2025-08-15 08:34:07 ❤️ by Enjin, 2017-2025 -2025-08-15 08:34:07 📋 Chain specification: Enjin Relaychain -2025-08-15 08:34:07 🏷 Node name: a_reasonably_unique_name_a8f310 -2025-08-15 08:34:07 👤 Role: AUTHORITY -2025-08-15 08:34:07 💾 Database: RocksDb at /enjin/chainstate/relaychain/chains/enjin-relaychain/db/full -[..] -2025-08-15 08:34:09 👶 Starting BABE Authorship worker -``` - -#### Generating Session Keys - -:::tip -If you do not have curl installed, you can install it using the following command: `sudo apt-get install curl` -::: - -You need to tell the chain about your Session Keys. This is what associates your validator node with your stash account. You can do this by signing and submitting an extrinsic. - -Before you can do this, you must first make add an additional option to allow for unsafe RPC methods to be performed: `--rpc-methods=unsafe` - -If you are using the binary, simply add this option to the end of your command. If you are using Docker, you will need to edit the `docker-compose.yml` file and the `command` field therein to include this. - -You must first call the `author_rotateKeys` on your node. You can do this via CLI using the following `curl` command: - -``` -curl -X POST \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"author_rotateKeys","params":[],"id":1}' \ - http://localhost:9944 -``` - -:::info -If you have changed the default ports for your node, you will need to modify the curl command to reflect which port exposes the RPC interface. -::: - -If you ran this successfully, you will receive a successful response and the result will include a hex-encoded string. This string is your Session Keys. It is important you make note of these keys as we will set it in the next session. - -`{"jsonrpc":"2.0","result":"0xef1b41a7fca69a26633deb436f3b11aa94eab64edbb9b41e290a1487bb9c9744[..]3893e7a76d3c7ec070a30c96ed1fd2b22daa6856c79ce709032891d4afd171c9","id":1}` - -**Make sure to remove the options exposing the RPC interface (`--rpc-external`, `--rpc-cors=all`, `--rpc-methods=unsafe`) that was added earlier as they are incompatible when running a node with `--validator`.** - -#### Bond ENJ, Set Session Keys and Begin Validating - -To become a Validator on the Enjin Relaychain, you initially need 15,000 ENJ. However, to become an Active Validator and earn rewards, you need a total stake that meets or exceeds a certain amount of ENJ. - -If you want community nominations, you must show commitment and trust by bonding some of your own ENJ and illustrating a willingness of own personal loss if you're to operate the Validator incorrectly. Don't bond all of your ENJ to ensure you can cover transaction fees. Bonded ENJ is locked and cannot be used. - -In this step, we will bond an amount ENJ, set our session keys and confirm we're ready to begin validating. To get started, navigate to the [Staking \> Accounts](https://console.enjin.io/?rpc=wss%3A%2F%2Frpc.relay.blockchain.enjin.io#/staking/actions) page on the Enjin Console and select `+ Add Validator`. - -![](/img/components/blockchain-infrastructure/2.png) - - -Select the `stash account` that you want to use as a Validator. This account will be responsible for bonding the relevant ENJ. - -Enter the amount that you would like to bond into the `value bonded` field. In this example we're using 15,000 ENJ. Remember that, if you want to be considered an Active Validator you must bond sufficient ENJ to exceed the current minimum active stake. - -For the `payment destination` there are three options: - -- **Stash account (increase the amount at stake):** this option allows your Validator's stake to keep increasing with each payout. This can be beneficial as it results in your Validator's stake (and, in-turn, your total stake) to keep increasing and thereby make you more likely to remain an Active Validator by being in the top 13. -- **Stash account (do not increase the amount at stake):** this option allows you to receive ENJ directly back to your stash account, though the amount is not added to your Validator's stake. -- **Specified payment account:** this option allows you to specify an alternative account (unrelated to the stash account) which will receive any payouts. -- You can then select `next` to continue the setup. - -![](/img/components/blockchain-infrastructure/3.png) - -In the `keys from rotatekeys` field, you want to enter the keys you acquired during Generating Session Keys. - -For the `reward commission percentage`, this is a value ranging from 1 - 100. This is the rate that your Validator will be commissioned with. - -Selecting a high commission percentage means you are less likely to be nominated by a nominator. Especially, during the early stages of you operating a Validator where proving your reputation is paramount. - -In the `allow new nominations` field. This controls whether you want to allow nomination pools to nominate your Validator. Selecting `Yes, allow nominations` will allow your Validator to be nominated by nominators. Selecting `No, block all nominations` will prevent your Validator from being nominated by any new nomination pools. - -Finally, select `Bond & Validate` to finalise the setup of your Validator. - -All that remains is to verify everything was set correctly. You can do this by navigating to the [Staking \> Overview](https://console.enjin.io/?rpc=wss%3A%2F%2Frpc.relay.blockchain.enjin.io#/staking) section of the Enjin Console and selecting the `Waiting` tab. If everything was successful, you'll see your Validator listed. - -![](/img/components/blockchain-infrastructure/4.png) - -Congratulations! If you've followed everything correctly, you're now officially a Validator! Make sure that you have enough total stake backing your Validator be selected for the next Validator Set. - -## FAQ - -### Validator Set - when are new Validators selected? - -Validators are selected at the start of the final epoch in a given era. For the Enjin Relaychain, this means on the 6th epoch (20th hour) of the era. In order to be selected, you must be one of the top 13 Validators based on total stake and not currently chilled or slashed. - -### When will my Validator become active? - -If you were selected to be part of the next Validator Set, your Validator will become active in the next era. For the Enjin Relaychain, this means 4 hours after your Validator was selected for inclusion within the next Validator Set. diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/02-security-best-practices-for-validators.md b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/02-security-best-practices-for-validators.md deleted file mode 100644 index e4e5aea..0000000 --- a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/02-security-best-practices-for-validators.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: "Security Best Practices for Validators" -slug: "security-best-practices" -description: "Validators play a critical role in the Enjin Blockchain ecosystem by proposing, validating and finalizing blocks. Given their importance, validators are frequent targets for various types of attacks. It is essential to follow best practices for setting up and maintaining your validator nodes to mitigate risk and ensure the network’s robustness." ---- -## Secure Server Configuration - -The security of the servers where validator nodes are run is fundamental to maintaining a secure validator. Here are the key considerations for securing your server: - -- **Use of Verified Software:** Always download Docker images from the official Docker Hub repository ([enjin/relaychain](https://hub.docker.com/r/enjin/relaychain)). This helps ensure that you are using the correct, unmodified, software necessary for network participation. -- **Up-to-Date Software:** Ensuring you’re running the latest stable release is important for both operating the node (you can sign up to our mailing list to keep up-to-date: [mailing-list-node-operators@enjin.io](https://groups.google.com/a/enjin.io/g/mailing-list-node-operators)), but also keeping your operating system and other applications up-to-date is equally essential to patch 0-days and prevent other such potential vulnerabilities. -- Verify File Integrity: After downloading files, verify the hashes against those provided by the download source. This step ensures that the files have not been tampered with or corrupted during the download process. -- **Follow Official Validator Setup Documentation:** Use the [Running a Validator](/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md) documentation to set up a validator node and ensure adhering to its principles. -- **Dedicated Server for Validator Nodes:** A validator node is an incredibly sensitive operation and running one requires careful considerations. One of those is ensuring the server running the validator node is dedicated for the sole purpose of running a single validator node. This is to ensure compute resources are dedicated to the node and to prevent other applications from interfering with the execution of the node. -- **Do Not Run as Privileged User:** Never use the root or admin user. Validators should run as a non-privileged user to minimize potential damage from attacks. - -### Linux-specific Best Practices - -It is strongly recommended that you disable non-essential SSH subsystems like banner, motd, scp, and X11 forwarding. Furthermore, preventing the use of password-based SSH in favor of key-based access will strongly improve your node’s security. - -## Network Security - -A well-configured network is crucial to protecting your validator from external threats. Below are network security best practices to follow: - -- **Firewalls and Port Management:** Protect your node from unauthorized access, you can read more on the [official documentation](/02-guides/02-blockchain/02-running-nodes/01-enjin-blockchain-nodes/01-running-a-node.md#ports) about which ports need to be opened and the rest can remain restricted. -- **Secure Access to Validator:** Ensure access to your validator is as restricted as possible. Use a Virtual Private Network (VPN) or SSH tunnel for secure remote access. Use IP whitelisting to allow only specific IP addresses to connect to your validator. When possible, make sure to enforce Multi-Factor Authentication (MFA) for any account with access to the validator. - -## Key Management and Secure Access - -Your validator node’s private key is the most sensitive asset. Proper key management and secure access protocol are essential to preventing key theft or misuse: - -- **Secure Private Key Storage and Generation:** Store your validator key on a Hardware Security Module (HSM) or a Hardware (Cold) Wallet to ensure it is highly secure and not exposed to the host operating system. -- **Validator Access Control:** Apply the principle of least privilege to all accounts and services. Ensure that users and processes have only the minimal level of access necessary to perform their functions. This reduces the risk of privilege escalation attacks or accidental key exposure to those that don't require access. -- **Regular Auditing:** reviewing access logs, security settings and applying ongoing updates based on changes in best practices. - -## Monitoring and Incident Response - -Continuous monitoring and an effective incident response are key to maintaining a secure validator setup. The following are best practices for setting up monitoring and incident response procedures: - -- **Real-Time Monitoring:** Implement real-time monitoring of your validator node, including performance, used resources (CPU, memory, disk), network traffic and validator health. Use tools like Prometheus and Grafana, and make sure to set up alerts for abnormal activities. -- **Logging and Auditing:** Implement comprehensive logging for your validator node. Store and maintain these logs securely. In the event of an issue or security incident, these logs can be audited to identify the root cause and facilitate a timely resolution. -- **Incident Response:** Create a team responsible for quick response to any incidents. The team should be able to answer incidents 24/7. -- **Backup and Recovery:** Implement a backup strategy that includes regular snapshots of the chainstate. Test recovery procedures periodically to ensure they are up-to-date and effective. -- **Validator Supervision:** Configure your validator software to be restarted automatically if stopped for any reason. Use a supervisor process to manage this. Review the logs associated with the commanded shutdown. - -## Conclusion - -Following these security best practices will help ensure your validator node is secure and compliant with the standards of the Enjin Blockchain. The security of validators is vital to maintaining the trust and functionality of the network. diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/03-how-to-stop-validating.md b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/03-how-to-stop-validating.md deleted file mode 100644 index dee0288..0000000 --- a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/03-how-to-stop-validating.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: "How to Stop Validating" -slug: "stop-validating" -description: "If you are currently operating a validator node on the Enjin Blockchain and wish to stop validating, follow the steps outlined below." ---- -Stopping validation may be necessary for various reasons, including switching roles, maintaining the node, upgrading the node, or withdrawing from the network as a validator. This guide will walk you through the process to ensure your transition out of the active set of validators is smooth and efficient. If you only wish to stop validating temporarily for maintenance, Step 1 is all you need to perform. If you wish to unbond funds and reap an account, Steps 1-3 should be followed. - -## Step 1: Chill Your Validator - -“Chilling” your validator means that it will no longer be an active participant in the validator set, effectively stopping it from validating blocks and earning rewards. Chilling does not affect your staked tokens immediately, it simply signals to the network that you wish to stop validation. It’s a few simple steps to chill your validator. - -:::danger Important -If your validator node is part of the active validator set, it is imperative that you **DO NOT** immediately turn off your validator. Chilling is a signal of intent and will ensure you’re not selected upon the next session selection. Wait until your validator node is no longer part of an active validator set before turning it offline. - -You may need to wait up to the conclusion of 7 epochs (28 hours) to be removed from being an active validator. -::: - -### Enjin Console - -- Open the [Enjin Console](https://console.enjin.io/#/staking/actions) with your validator account. -- Navigate to Network \> Staking \> Account. -- Click “Stop” next to your validator. - -### Programmatically - -- Connect to the Enjin RPC node with your validator account. -- Send the `staking.chill()` extrinsic. - -Once your validator is chilled, it will no longer be eligible for selection in future staking sessions and it will stop producing blocks. - -## Step 2: Purge Validator Session Keys - -:::danger Important -Purging session keys is a permanent action. Once completed, your validator will no longer be able to participate in validation activities unless new keys are generated and [set up again](/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/01-running-a-validator.md). - -This step is crucial for security, as it prevents any accidental or unauthorized activation of your validator. -::: - -After chilling your validator, the next crucial step is to purge the session keys associated with your validator. Purging the session keys removes association of your validator node with your stash account. - -- Open [Enjin Console](https://console.enjin.io/#/extrinsics). -- Navigate to Developer \> Extrinsics. -- Select the Correct Account. It should be the same account that was used to set the keys in the first place.Call the `session.purge_keys()` extrinsic. - -## Step 3: Unbond Your Tokens - -Unbonding will release your ENJ staked as a validator. Once unbonded, your tokens will become freely transferable. You can follow the below steps to unbond your staked ENJ. - -### Enjin Console - -- Open the Enjin Console with your stash account. -- Navigate to Network \> Staking \> Account actions. -- Click the corresponding stash account dropdown and select “Unbond funds”. - -### Programmatically - -- Connect to the Enjin RPC node with your stash account. -- Send `staking.unbond()` extrinsic. - -## Conclusion - -To temporarily stop validating on the Enjin Blockchain you have only to chill your validator. - -To permanently stop validating, you must follow three primary steps: chilling your validator, purging the validator’s session keys, and unbonding your tokens. - -By following these steps carefully, you can ensure a secure and smooth transition out of your role as a validator without any accidental penalties or disruptions. diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/_category_.json b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/_category_.json deleted file mode 100644 index d9d1e76..0000000 --- a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Operating a Relaychain Validator", - "link": { - "type": "generated-index", - "description": "This section takes you on the journey of operating a Relaychain Validator Node." - } -} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/_category_.json b/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/_category_.json deleted file mode 100644 index 68ca060..0000000 --- a/versioned_docs/version-2.x/02-guides/02-blockchain/02-running-nodes/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Running Nodes", - "link": { - "type": "generated-index", - "description": "Learn how to set up and operate various types of nodes and validators on the Enjin Blockchain, including relay chain and matrix chain nodes." - } -} \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/03-running-enjin-indexer.md b/versioned_docs/version-2.x/02-guides/02-blockchain/03-running-enjin-indexer.md deleted file mode 100644 index 319c11a..0000000 --- a/versioned_docs/version-2.x/02-guides/02-blockchain/03-running-enjin-indexer.md +++ /dev/null @@ -1,220 +0,0 @@ ---- -title: "Running the Enjin Indexer" -slug: "running-enjin-indexer" -description: "The Enjin Indexer is a tool that processes and serves blockchain data for applications that require efficient data retrieval, filtering, and relations. This guide will walk you through setting up and running the indexer step-by-step." ---- -## Prerequisites - -Before you begin, ensure that you have [installed Docker](https://docs.docker.com/get-started/get-docker/) on a machine you will use to host the Indexer. - -## Quick Start - -1. Begin by creating three files: (`docker-compose.yml`, `.env` and `init.sql`): - -```yaml -# docker-composer.yml - -version: "3.9" - -services: - indexer_db: - container_name: indexer_db - image: postgres:16 - restart: unless-stopped - environment: - POSTGRES_DB: ${DB_NAME} - POSTGRES_PASSWORD: ${DB_PASS} - volumes: - - ./init.sql:/docker-entrypoint-initdb.d/init.sql - - indexer_db:/var/lib/postgresql/data - ports: - - "5432:5432" - command: [ - "postgres", - "-c", "max_connections=200" - ] - - indexer_redis: - container_name: indexer_redis - image: redis:7.0 - restart: on-failure - volumes: - - redis_db:/data - ports: - - "6389:6379" - - indexer_processor: - container_name: indexer_processor - restart: unless-stopped - image: enjin/indexer:${VERSION:-latest} - environment: - CONTAINER_ROLE: processor - build: - context: . - volumes: - - .env:/squid/.env - depends_on: - - indexer_db - - indexer_worker - - indexer_graphql: - container_name: indexer_graphql - restart: unless-stopped - image: enjin/indexer:${VERSION:-latest} - environment: - CONTAINER_ROLE: graphql - build: - context: . - ports: - - "4000:4000" - - "8080:8080" - volumes: - - .env:/squid/.env - depends_on: - - indexer_db - - indexer_processor - - indexer_worker: - container_name: indexer_worker - restart: unless-stopped - image: enjin/indexer:${VERSION:-latest} - environment: - CONTAINER_ROLE: worker - build: - context: . - ports: - - "9090:9090" - volumes: - - .env:/squid/.env - depends_on: - - indexer_db - - indexer_redis - -volumes: - indexer_db: - redis_db: -``` - -```sql --- init.sql - -CREATE SCHEMA IF NOT EXISTS metadata; - -CREATE TABLE IF NOT EXISTS "metadata"."metadata" ( - id TEXT PRIMARY KEY, - metadata JSONB, - uri TEXT, - last_updated_at TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS metadata_uri ON "metadata"."metadata" (uri); -``` - -### .dotenv Configurations - -#### Enjin Relaychain -```dotenv -# .env (for Enjin Relaychain) - -ARCHIVE_ENDPOINT=https://v2.archive.subsquid.io/network/enjin-relay -CHAIN_ENDPOINT=wss://archive.relay.blockchain.enjin.io -CHAIN_PREFIX=2135 -DB_HOST=indexer_db -DB_NAME=indexer_enjin_relay -DB_PORT=5432 -DB_PASS=postgres -REDIS_URL=redis://indexer_redis:6379/0 -``` - -#### Enjin Matrixchain -```dotenv -# .env (for Enjin Matrixchain) - -ARCHIVE_ENDPOINT=https://v2.archive.subsquid.io/network/enjin-matrix -CHAIN_ENDPOINT=wss://archive.matrix.blockchain.enjin.io -CHAIN_PREFIX=1110 -DB_HOST=indexer_db -DB_NAME=indexer_enjin_matrix -DB_PORT=5432 -DB_PASS=postgres -REDIS_URL=redis://indexer_redis:6379/1 -``` - -#### Canary Relaychain -```dotenv -# .env (for Canary Relaychain) - -ARCHIVE_ENDPOINT=# no archive endpoint available for Canary Relaychain -CHAIN_ENDPOINT=wss://archive.relay.canary.enjin.io -CHAIN_PREFIX=69 -DB_HOST=indexer_db -DB_NAME=indexer_canary_relay -DB_PORT=5432 -DB_PASS=postgres -REDIS_URL=redis://indexer_redis:6379/2 -``` - -#### Canary Matrixchain -```dotenv -# .env (for Canary Matrixchain) - -ARCHIVE_ENDPOINT=https://v2.archive.subsquid.io/network/enjin-canary-matrix -CHAIN_ENDPOINT=wss://archive.matrix.canary.enjin.io -CHAIN_PREFIX=9030 -DB_HOST=indexer_db -DB_NAME=indexer_canary_matrix -DB_PORT=5432 -DB_PASS=postgres -REDIS_URL=redis://indexer_redis:6379/3 -``` -
- -2. Start the Indexer by running the following command: `docker compose up -d` - _The command will launch all the necessary services in the background._ -3. Access the GraphQL API - Once the services are up and running, you can access the GraphQL Playground of the Indexer by opening your web browser and navigating to: http://localhost:4000/graphql - -You can read the [GraphQL Documentation](https://graphql.org/learn/) for more information about how to construct queries for the GraphQL interface. - -### Advanced Configuration - -1. Clone and Enter the Repository - ```bash - git clone https://github.com/enjin/indexer.git - cd indexer - ``` -2. Configure Environment Variables - In the repository, you will find a `.env.example` file that outlines the necessary environment variables for running the Enjin Indexer. Copy this file to create your own `.env` file: `cp .env.example .env` - Then, open the .env file in a text editor and fill in the missing values. Below is an example of the environment variables and their purpose: - -```text -NODE_ENV: Node.js environment (supported values: development, production) - -CHAIN_ENDPOINT: [Required] WebSocket endpoint to connect to. It is recommended to be set to your own Enjin archive node. -CHAIN_PREFIX: [Required] The chain prefix (SS58) of the chain you are connecting to. -ARCHIVE_ENDPOINT: [Optional] Subsquid archive endpoint for historical blockchain data. See: https://docs.sqd.ai/subsquid-network/reference/networks/https://docs.sqd.ai/subsquid-network/reference/networks/ - -DB_NAME: [Required] Postgres database name. -DB_HOST: [Required] Postgres host. -DB_PORT: [Required] Postgres port. -DB_PASS: [Required] Postgres password. -REDIS_URL: [Required] Redis URL. - -PROMETHEUS_PORT: [Optional] Prometheus port for metrics. -PROCESSOR_PROMETHEUS_ENDPOINT: [Optional] Prometheus endpoint for the processor. - -SQD_DEBUG: [Optional] Debugging flag for Subsquid. More info can be found at: https://docs.sqd.dev/sdk/reference/logger/ -SENTRY_DSN: [Optional] Sentry DSN for error tracking. -``` - -:::info -If you are using your own Redis or PostgresSQL instances (instead of the ones provided by Docker Compose), remember to remove the `indexer_db` and `indexer_redis` services from the `docker-compose.yml`, and set the database connection details (like `DB_HOST`, `DB_PORT` and `REDIS_URL`) in the `.env` file to match your external services. -::: - -## Troubleshooting - -- Ensure Docker is installed and running with sufficient resources. -- Use docker `compose logs -f ` to view logs and diagnose issues. -- Double-check all environment variables and configurations in the `.env` file. - -For additional support, you can reach out to [Enjin Support](https://support.enjin.io). diff --git a/versioned_docs/version-2.x/02-guides/02-blockchain/_category_.json b/versioned_docs/version-2.x/02-guides/02-blockchain/_category_.json deleted file mode 100644 index d034661..0000000 --- a/versioned_docs/version-2.x/02-guides/02-blockchain/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "Blockchain", - "link": { - "type": "generated-index", - "description": "Explore guides for running and operating components of the Enjin Blockchain. Learn how to set up nodes, operate validators, utilize the Matrixchain Indexer, and engage with staking and nomination pools on the Relaychain." - } -} - \ No newline at end of file diff --git a/versioned_docs/version-2.x/02-guides/_category_.json b/versioned_docs/version-2.x/02-guides/_category_.json deleted file mode 100644 index 7952236..0000000 --- a/versioned_docs/version-2.x/02-guides/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Integration Guides", - "link": { - "type": "generated-index", - "description": "Integrate with Enjin's platform and blockchain technologies. Find guides on managing tokens, integrating users, running nodes, and using the SDK." - } -} \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/01-queries/01-transactions-queries.md b/versioned_docs/version-2.x/03-api-reference/01-queries/01-transactions-queries.md deleted file mode 100644 index 1d274ad..0000000 --- a/versioned_docs/version-2.x/03-api-reference/01-queries/01-transactions-queries.md +++ /dev/null @@ -1,1116 +0,0 @@ ---- -title: "Transactions" -slug: "transactions" -description: "Use the Enjin API to query transaction data, including blockchain transactions history, status, and details." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - -:::tip Core Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.enjin.io/graphql` -::: - -This is an overview of some of the most commonly used operations in the Enjin Platform Schema. - -## GetBlocks - -The `GetBlocks` query allows you to retrieve an array of blocks(), which can be filtered based on specific block numbers, transaction IDs, or hashes. It is designed to fetch detailed data on a set of blocks, providing valuable insights. - - - -```graphql -query GetBlocks { - GetBlocks(numbers: ["402865"]) { - totalCount - pageInfo { - startCursor - endCursor - hasPreviousPage - hasNextPage - } - edges { - cursor - node { - id - number - hash - synced - failed - exception - } - } - } -} -``` - - -```json -{ - "data": { - "GetBlocks": { - "totalCount": 1, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasPreviousPage": false, - "hasNextPage": false - }, - "edges": [ - { - "cursor": "eyJudW1iZXIiOjQwMjg2NSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 51507, - "number": 402865, - "hash": "0xf0b3cee1c36a99e24aaef7da06aec2ecd79a599c19ad4ae6fb4b40b3e497a322", - "synced": true, - "failed": false, - "exception": null - } - } - ] - } - } -} -``` - - - -## GetTransaction - -The `GetTransaction` query allows you to retrieve detailed information about a specific transaction. It provides comprehensive details about the transaction, including its state, associated wallet, and related events. - - - -```graphql -query GetTransaction { - GetTransaction( - id: 11300 - ) { - id - idempotencyKey - transactionId - transactionHash - method - state - result - encodedData - signedAtBlock - createdAt - updatedAt - wallet { - account { - publicKey - address - } - } - events { - edges { - cursor - node { - phase - lookUp - moduleId - eventId - params { - type - value - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } - } -} -``` - - -```json -{ - "data": { - "GetTransaction": { - "id": 11300, - "idempotencyKey": "61f0b8e1-e0b9-4ecd-b445-84dc4cd2b495", - "transactionId": "402811-2", - "transactionHash": "0xbafe459e8248b802f3aef98d2e4a695bbb238899edf40519b082366e3ff8b98f", - "method": "BatchMint", - "state": "FINALIZED", - "result": "EXTRINSIC_SUCCESS", - "encodedData": "0x280dc56f04985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d05000180400010000c16ff2862300000000000000000001000000000000", - "signedAtBlock": 402809, - "createdAt": "2023-06-23T05:20:52+00:00", - "updatedAt": "2023-06-23T05:21:26+00:00", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "events": { - "edges": [ - { - "cursor": "eyJpZCI6Njk3MTEsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "Balances", - "eventId": "Withdraw", - "params": [ - { - "type": "who", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "amount", - "value": "19981763653182275" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTIsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "Balances", - "eventId": "Reserved", - "params": [ - { - "type": "who", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "amount", - "value": "10000000000000000" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTMsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "MultiTokens", - "eventId": "TokenCreated", - "params": [ - { - "type": "collection_id", - "value": "7153" - }, - { - "type": "token_id", - "value": "6" - }, - { - "type": "issuer", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "initial_supply", - "value": "1" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTQsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "MultiTokens", - "eventId": "TokenAccountCreated", - "params": [ - { - "type": "collection_id", - "value": "7153" - }, - { - "type": "token_id", - "value": "6" - }, - { - "type": "account", - "value": "985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050" - }, - { - "type": "balance", - "value": "1" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "MultiTokens", - "eventId": "Minted", - "params": [ - { - "type": "collection_id", - "value": "7153" - }, - { - "type": "token_id", - "value": "6" - }, - { - "type": "issuer", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "recipient", - "value": "985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050" - }, - { - "type": "amount", - "value": "1" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTYsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "Balances", - "eventId": "Deposit", - "params": [ - { - "type": "who", - "value": "6d6f646c65662f66656469730000000000000000000000000000000000000000" - }, - { - "type": "amount", - "value": "19981763653182275" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTcsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "TransactionPayment", - "eventId": "TransactionFeePaid", - "params": [ - { - "type": "who", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "actual_fee", - "value": "19981763653182275" - }, - { - "type": "tip", - "value": "0" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTgsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "System", - "eventId": "ExtrinsicSuccess", - "params": [ - { - "type": "dispatch_info", - "value": "{\"weight\":{\"ref_time\":\"848559950\",\"proof_size\":\"10283\"},\"class\":\"Normal\",\"pays_fee\":\"Yes\"}" - } - ] - } - } - ], - "totalCount": 8, - "pageInfo": { - "hasPreviousPage": false, - "hasNextPage": false, - "startCursor": "", - "endCursor": "" - } - } - } - } -} -``` - - - -## GetTransactions - -The `GetTransactions` query in allows you to retrieve an array of transactions. You can optionally filter these transactions based on various parameters such as transaction IDs, transaction hashes, methods, states, results, or accounts. - - - -```graphql -query GetTransactions { - GetTransactions( - ids: [11300] - ) { - edges { - cursor - node { - id - idempotencyKey - transactionId - transactionHash - method - state - result - encodedData - signedAtBlock - createdAt - updatedAt - wallet { - account { - publicKey - address - } - } - events { - edges { - cursor - node { - phase - lookUp - moduleId - eventId - params { - type - value - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } - } - } - totalCount - pageInfo { - startCursor - endCursor - hasPreviousPage - hasNextPage - } - } -} -``` - - -```json -{ - "data": { - "GetTransactions": { - "edges": [ - { - "cursor": "eyJpZCI6MTEzMDAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "id": 11300, - "idempotencyKey": "61f0b8e1-e0b9-4ecd-b445-84dc4cd2b495", - "transactionId": "402811-2", - "transactionHash": "0xbafe459e8248b802f3aef98d2e4a695bbb238899edf40519b082366e3ff8b98f", - "method": "BatchMint", - "state": "FINALIZED", - "result": "EXTRINSIC_SUCCESS", - "encodedData": "0x280dc56f04985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d05000180400010000c16ff2862300000000000000000001000000000000", - "signedAtBlock": 402809, - "createdAt": "2023-06-23T05:20:52+00:00", - "updatedAt": "2023-06-23T05:21:26+00:00", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "events": { - "edges": [ - { - "cursor": "eyJpZCI6Njk3MTEsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "Balances", - "eventId": "Withdraw", - "params": [ - { - "type": "who", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "amount", - "value": "19981763653182275" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTIsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "Balances", - "eventId": "Reserved", - "params": [ - { - "type": "who", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "amount", - "value": "10000000000000000" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTMsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "MultiTokens", - "eventId": "TokenCreated", - "params": [ - { - "type": "collection_id", - "value": "7153" - }, - { - "type": "token_id", - "value": "6" - }, - { - "type": "issuer", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "initial_supply", - "value": "1" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTQsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "MultiTokens", - "eventId": "TokenAccountCreated", - "params": [ - { - "type": "collection_id", - "value": "7153" - }, - { - "type": "token_id", - "value": "6" - }, - { - "type": "account", - "value": "985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050" - }, - { - "type": "balance", - "value": "1" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "MultiTokens", - "eventId": "Minted", - "params": [ - { - "type": "collection_id", - "value": "7153" - }, - { - "type": "token_id", - "value": "6" - }, - { - "type": "issuer", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "recipient", - "value": "985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050" - }, - { - "type": "amount", - "value": "1" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTYsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "Balances", - "eventId": "Deposit", - "params": [ - { - "type": "who", - "value": "6d6f646c65662f66656469730000000000000000000000000000000000000000" - }, - { - "type": "amount", - "value": "19981763653182275" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTcsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "TransactionPayment", - "eventId": "TransactionFeePaid", - "params": [ - { - "type": "who", - "value": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160" - }, - { - "type": "actual_fee", - "value": "19981763653182275" - }, - { - "type": "tip", - "value": "0" - } - ] - } - }, - { - "cursor": "eyJpZCI6Njk3MTgsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "phase": 2, - "lookUp": "unknown", - "moduleId": "System", - "eventId": "ExtrinsicSuccess", - "params": [ - { - "type": "dispatch_info", - "value": "{\"weight\":{\"ref_time\":\"848559950\",\"proof_size\":\"10283\"},\"class\":\"Normal\",\"pays_fee\":\"Yes\"}" - } - ] - } - } - ], - "totalCount": 8, - "pageInfo": { - "hasPreviousPage": false, - "hasNextPage": false, - "startCursor": "", - "endCursor": "" - } - } - } - } - ], - "totalCount": 1, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasPreviousPage": false, - "hasNextPage": false - } - } - } -} -``` - - - -## GetPendingEvents - -The `GetPendingEvents` query is designed to retrieve a list of events that have been broadcast by the system but not yet acknowledged by the client. This is useful for ensuring that no events are missed or unprocessed, particularly in systems that rely on event-driven architectures or asynchronous processing. - - - -```graphql -query GetPendingEvents{ - GetPendingEvents( - acknowledgeEvents: false - ) { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - id - uuid - name - sent - channels - data - } - } - } -} -``` - - -```json -{ - "data": { - "GetPendingEvents": { - "totalCount": 95151, - "pageInfo": { - "startCursor": "", - "endCursor": "eyJpZCI6NjA3LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "hasNextPage": true, - "hasPreviousPage": false - }, - "edges": [ - { - "cursor": "eyJpZCI6NTkzLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 593, - "uuid": "b7678fd9-8447-453b-8821-172d6b84397b", - "name": "platform:beam-claims-complete", - "sent": "2023-05-28 15:08:07", - "channels": [ - "collection;101729", - "platform", - "beam;platform" - ], - "data": { - "id": 11, - "beam": { - "id": 2, - "end": "2028-11-14 00:00:00", - "code": "aa9c8deb3549e635ef2fc40a7285596a", - "name": "373", - "image": "https://en.wikipedia.org/wiki/Firefox#/media/File:Firefox_logo,_2019.svg", - "start": "2022-11-14 00:00:00", - "flags_mask": 0, - "description": "This is the most awesome Beam ever.", - "collection_chain_id": "101729" - }, - "type": "MINT_ON_DEMAND", - "state": "COMPLETED", - "quantity": 10, - "attributes": [], - "claimed_at": "2023-05-28 15:07:37", - "ip_address": "172.69.58.64", - "token_chain_id": "1283", - "wallet_public_key": "0x83800dc53431f36f0aaaad573eded3394c2c976d28d528111f82968f0a601bc2" - } - } - }, - { - "cursor": "eyJpZCI6NTk0LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 594, - "uuid": "50859a2b-574e-4d0d-8577-e8e53f9eee1d", - "name": "platform:transaction-created", - "sent": "2023-05-28 15:08:10", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 98, - "state": "PENDING", - "method": "CreateCollection", - "idempotencyKey": "575992c4-911a-4d59-8ccf-7958d46e605b" - } - } - }, - { - "cursor": "eyJpZCI6NTk1LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 595, - "uuid": "63c4c28d-463c-4455-bb9b-43ccbf89b53a", - "name": "platform:transaction-created", - "sent": "2023-05-28 15:08:10", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 99, - "state": "PENDING", - "method": "BatchSetAttribute", - "idempotencyKey": "0134b2af-b92d-4999-8900-3f8124172247" - } - } - }, - { - "cursor": "eyJpZCI6NTk2LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 596, - "uuid": "fe37740b-cb5b-4605-940a-d2b6927ca2d6", - "name": "platform:beam-claim-pending", - "sent": "2023-05-28 15:08:13", - "channels": [ - "collection;101729", - "platform", - "beam;platform" - ], - "data": { - "beam": { - "id": 2, - "end": "2028-11-14 00:00:00", - "code": "aa9c8deb3549e635ef2fc40a7285596a", - "name": "373", - "image": "https://en.wikipedia.org/wiki/Firefox#/media/File:Firefox_logo,_2019.svg", - "start": "2022-11-14 00:00:00", - "flags_mask": 0, - "description": "This is the most awesome Beam ever.", - "collection_chain_id": "101729" - }, - "state": "PENDING", - "extras": [], - "beam_id": 2, - "claimed_at": "2023-05-28T15:08:13.679083Z", - "ip_address": "172.70.126.55", - "idempotency_key": "0b193cff-8c97-4a72-881b-1adb4432da1d", - "wallet_public_key": "0x66f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f646009" - } - } - }, - { - "cursor": "eyJpZCI6NTk3LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 597, - "uuid": "b3923e2b-3f46-4166-951d-d074c05255a1", - "name": "platform:transaction-updated", - "sent": "2023-05-28 15:08:16", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 99, - "state": "PROCESSING", - "method": "BatchSetAttribute", - "result": null, - "transactionId": null, - "idempotencyKey": "0134b2af-b92d-4999-8900-3f8124172247", - "transactionHash": null - } - } - }, - { - "cursor": "eyJpZCI6NTk4LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 598, - "uuid": "9f4911be-3b0e-4604-8248-01db2f6c9255", - "name": "platform:transaction-updated", - "sent": "2023-05-28 15:08:16", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 98, - "state": "PROCESSING", - "method": "CreateCollection", - "result": null, - "transactionId": null, - "idempotencyKey": "575992c4-911a-4d59-8ccf-7958d46e605b", - "transactionHash": null - } - } - }, - { - "cursor": "eyJpZCI6NTk5LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 599, - "uuid": "b6cfd3bc-5850-415b-9db6-06f7ebea6f50", - "name": "platform:transaction-created", - "sent": "2023-05-28 15:08:16", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 100, - "state": "PENDING", - "method": "BatchMint", - "idempotencyKey": "b09251c5-7911-4d0c-ac02-89ab049aa713" - } - } - }, - { - "cursor": "eyJpZCI6NjAwLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 600, - "uuid": "2de8a2b2-10e7-4769-a432-53c6258e8721", - "name": "platform:beam-claim-in-progress", - "sent": "2023-05-28 15:08:16", - "channels": [ - "collection;101729", - "platform", - "beam;platform" - ], - "data": { - "id": 20, - "beam": { - "id": 2, - "end": "2028-11-14 00:00:00", - "code": "aa9c8deb3549e635ef2fc40a7285596a", - "name": "373", - "image": "https://en.wikipedia.org/wiki/Firefox#/media/File:Firefox_logo,_2019.svg", - "start": "2022-11-14 00:00:00", - "flags_mask": 0, - "description": "This is the most awesome Beam ever.", - "collection_chain_id": "101729" - }, - "quantity": 10, - "attributes": [], - "collection": { - "id": 98684, - "owner": { - "id": 2, - "address": "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", - "managed": 0, - "network": "developer", - "created_at": "2023-05-28T14:47:39.000000Z", - "public_key": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "updated_at": "2023-05-28T14:47:39.000000Z", - "external_id": null, - "verification_id": null - }, - "network": "developer", - "is_frozen": 0, - "created_at": "2023-05-28T15:06:30.000000Z", - "updated_at": "2023-05-28T15:06:30.000000Z", - "token_count": "0", - "total_deposit": "25000000000000000000", - "attribute_count": "0", - "max_token_count": "100", - "owner_wallet_id": 2, - "max_token_supply": "100", - "force_single_mint": 0, - "royalty_wallet_id": null, - "royalty_percentage": null, - "collection_chain_id": "101729" - }, - "token_chain_id": "1283", - "wallet_public_key": "0x66f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f646009" - } - } - }, - { - "cursor": "eyJpZCI6NjAxLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 601, - "uuid": "50d2f458-0565-41c7-94d3-c011f7528c9e", - "name": "platform:transaction-updated", - "sent": "2023-05-28 15:08:16", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 99, - "state": "BROADCAST", - "method": "BatchSetAttribute", - "result": null, - "transactionId": null, - "idempotencyKey": "0134b2af-b92d-4999-8900-3f8124172247", - "transactionHash": "0xd4e253623bc8170eef9cbe4537c08e063bd421ebb18b9943b9db7b75c5a0c03e" - } - } - }, - { - "cursor": "eyJpZCI6NjAyLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 602, - "uuid": "10b155ce-e514-4db8-9702-d1df475903a9", - "name": "platform:transaction-updated", - "sent": "2023-05-28 15:08:16", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 98, - "state": "BROADCAST", - "method": "CreateCollection", - "result": null, - "transactionId": null, - "idempotencyKey": "575992c4-911a-4d59-8ccf-7958d46e605b", - "transactionHash": "0xafecedea403e28722fb454b9be9bca5c55a9d0d7a62c30d80f2e2e64f78c4436" - } - } - }, - { - "cursor": "eyJpZCI6NjAzLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 603, - "uuid": "a6f22ab2-fd62-45a0-9c24-99b5c814f6b1", - "name": "platform:token-burned", - "sent": "2023-05-28 15:08:19", - "channels": [ - "collection;101728", - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", - "token;70468841277235617716769448539773927607", - "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "platform" - ], - "data": { - "amount": "1", - "wallet": "68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "tokenId": "70468841277235617716769448539773927607", - "collectionId": "101728", - "idempotencyKey": "4259ee51-f38e-4e57-907d-3da671340917" - } - } - }, - { - "cursor": "eyJpZCI6NjA0LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 604, - "uuid": "c7703b9d-08cd-4350-b342-2c263975cde0", - "name": "platform:collection-created", - "sent": "2023-05-28 15:08:19", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", - "platform" - ], - "data": { - "owner": "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", - "collectionId": "101732", - "idempotencyKey": "7c909267-1ff3-45c8-9651-b87a1697042a" - } - } - }, - { - "cursor": "eyJpZCI6NjA1LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 605, - "uuid": "2b8ac300-d970-4a56-8db5-e979d0827f47", - "name": "platform:collection-created", - "sent": "2023-05-28 15:08:19", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", - "platform" - ], - "data": { - "owner": "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe", - "collectionId": "101733", - "idempotencyKey": "492af562-c4e9-418d-9707-5ce4a24680e1" - } - } - }, - { - "cursor": "eyJpZCI6NjA2LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 606, - "uuid": "cfeca74d-b0cf-42d9-bb5a-b8bf553b3127", - "name": "platform:transaction-updated", - "sent": "2023-05-28 15:08:19", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 97, - "state": "FINALIZED", - "method": "Burn", - "result": "EXTRINSIC_SUCCESS", - "transactionId": "865545-2", - "idempotencyKey": "4259ee51-f38e-4e57-907d-3da671340917", - "transactionHash": "0xd6f47b7c9ef3479b788ecaf76a203ebbefdb96f74fce3cac75ad84b0fc49f191" - } - } - }, - { - "cursor": "eyJpZCI6NjA3LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "id": 607, - "uuid": "98a3449f-ccba-4f73-9a33-d4d9d55f979f", - "name": "platform:transaction-updated", - "sent": "2023-05-28 15:08:19", - "channels": [ - "rf67pPeLYBJRfrehJzzAPVypSCUpPYE62v1gT3f6isBC2EXYe" - ], - "data": { - "id": 95, - "state": "FINALIZED", - "method": "CreateCollection", - "result": "EXTRINSIC_SUCCESS", - "transactionId": "865545-3", - "idempotencyKey": "7c909267-1ff3-45c8-9651-b87a1697042a", - "transactionHash": "0x6b1cdfaafcc5b343c02ce4593d56e0ad3bf2ffc814baac5e699a43e73c147a21" - } - } - } - ] - } - } -} -``` - - - - -### Use Case: - -You can use the `GetPendingEvents` query to ensure that all events are accounted for and processed in your system, even in the presence of network issues or disruptions. This query is crucial for: - -- Reconciling transaction records. -- Updating user interfaces in real-time. -- Triggering downstream processes that depend on event data. - -Developers and system administrators can use this query to monitor and handle event-driven systems effectively, maintaining their reliability and consistency. To acknowledge the outputted events (and therefore remove them from pending events), set the `acknowledgeEvents` parameter to `true` (default is false). -To filter outputted pending events on specific channels, use the `channelFilters` parameter - -```graphql -query geFilteredtPendingEvents { - GetPendingEvents( - channelFilters:[ - {type:OR filter:"cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu"} - {type:OR filter:"cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f"} - ]){ - edges { - node { - id - uuid - data - channels - } - } - } -} -``` - -This example will output pending events broadcasted in either `cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu` and `cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f` channels. -Using the `Type` argument, you, can construct advanced filters like `'channel_1' AND ('channel_2' OR 'channel_3')` -Note, The default type is "**AND**". diff --git a/versioned_docs/version-2.x/03-api-reference/01-queries/02-collections-queries.md b/versioned_docs/version-2.x/03-api-reference/01-queries/02-collections-queries.md deleted file mode 100644 index eb5c740..0000000 --- a/versioned_docs/version-2.x/03-api-reference/01-queries/02-collections-queries.md +++ /dev/null @@ -1,405 +0,0 @@ ---- -title: "Collections" -slug: "collections" -description: "Query the Enjin API to retrieve data about collections, including metadata and token information within a collection." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - -:::tip Core Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.enjin.io/graphql` -::: - -This is a detailed reference guide that explains the most commonly used operations. - -## Get Collection - -The `GetCollection` query allows you to retrieve detailed information about a specific collection by providing its `collectionId`. This information includes collection attributes, token details, and associated accounts. - -:::warning Reading Third-Party Collections -Please note that the Enjin Platform Cloud is set up to show only the collections and tokens that were created via the auth-ed Enjin Platform Cloud account.\ -To get a collection that was created elsewhere (via a different Enjin Platform Cloud account / [NFT.io](https://nft.io) / [Enjin Console](https://console.enjin.io) / etc.) the collection must be "Tracked" first, or the query response will return an error.\ -Learn more about tracking a collection in the [Tracking Collections section](/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md#tracking-collections). -::: - - - -```graphql -query GetCollection { - GetCollection(collectionId: 7153) { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - owner { - account { - publicKey - address - } - } - attributes { - key - value - } - tokens { - edges { - cursor - node { - tokenId - } - } - totalCount - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - } - accounts { - edges { - cursor - node { - accountCount - isFrozen - wallet { - account { - publicKey - address - } - } - approvals { - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - totalCount - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - } - } -} -``` - - -```json -{ - "data": { - "GetCollection": { - "collectionId": "7153", - "maxTokenCount": 100, - "maxTokenSupply": "100", - "forceSingleMint": false, - "frozen": false, - "network": "canary", - "owner": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "attributes": [], - "tokens": { - "edges": [ - { - "cursor": "eyJpZCI6NzI3MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "tokenId": "10" - } - }, - { - "cursor": "eyJpZCI6NzI3MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "tokenId": "6" - } - }, - { - "cursor": "eyJpZCI6Mzk0MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "tokenId": "68" - } - }, - { - "cursor": "eyJpZCI6Mzk1MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "tokenId": "70468841277235617716769448539773927607" - } - } - ], - "totalCount": 4, - "pageInfo": { - "hasNextPage": false, - "hasPreviousPage": false, - "startCursor": "", - "endCursor": "" - } - }, - "accounts": { - "edges": [ - { - "cursor": "eyJpZCI6MTM0OCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "accountCount": 2, - "isFrozen": false, - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "approvals": [] - } - }, - { - "cursor": "eyJpZCI6MjQ2MSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "accountCount": 2, - "isFrozen": false, - "wallet": { - "account": { - "publicKey": "0x985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050", - "address": "cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts" - } - }, - "approvals": [] - } - } - ], - "totalCount": 2, - "pageInfo": { - "hasNextPage": false, - "hasPreviousPage": false, - "startCursor": "", - "endCursor": "" - } - } - } - } -} -``` - - - -## GetCollections - -:::warning 🚧 Using the Enjin Platform Cloud? -The Enjin Platform Cloud has collections and tokens scoping enabled, to ensure a better experience by only showing you collections and tokens you created using the cloud.\ -If you wish to fetch collections/tokens created outside of your cloud account, consider transitioning to the [Enterprise On-Prem Enjin Platform](/05-enjin-platform/04-enterprise-on-prem/04-enterprise-on-prem.md). -::: - -The `GetCollections` query allows you to retrieve an array of collections. You can optionally filter the collections by providing a list of collection IDs that you are interested in. - - - -```graphql -query GetCollections { - GetCollections(collectionIds: [7153]) { - edges { - cursor - node { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - owner { - account { - publicKey - address - } - } - attributes { - key - value - } - tokens { - edges { - cursor - node { - tokenId - } - } - totalCount - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - } - accounts { - edges { - cursor - node { - accountCount - isFrozen - wallet { - account { - publicKey - address - } - } - approvals { - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - totalCount - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - } - } - } - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - totalCount - } -} -``` - - -```json -{ - "data": { - "GetCollections": { - "edges": [ - { - "cursor": "eyJjb2xsZWN0aW9uX2lkIjo3MTUzLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "collectionId": "7153", - "maxTokenCount": 100, - "maxTokenSupply": "100", - "forceSingleMint": false, - "frozen": false, - "network": "canary", - "owner": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "attributes": [], - "tokens": { - "edges": [ - { - "cursor": "eyJpZCI6NzI3MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "tokenId": "10" - } - }, - { - "cursor": "eyJpZCI6NzI3MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "tokenId": "6" - } - }, - { - "cursor": "eyJpZCI6Mzk0MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "tokenId": "68" - } - }, - { - "cursor": "eyJpZCI6Mzk1MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "tokenId": "70468841277235617716769448539773927607" - } - } - ], - "totalCount": 4, - "pageInfo": { - "hasNextPage": false, - "hasPreviousPage": false, - "startCursor": "", - "endCursor": "" - } - }, - "accounts": { - "edges": [ - { - "cursor": "eyJpZCI6MTM0OCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "accountCount": 2, - "isFrozen": false, - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "approvals": [] - } - }, - { - "cursor": "eyJpZCI6MjQ2MSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "accountCount": 2, - "isFrozen": false, - "wallet": { - "account": { - "publicKey": "0x985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050", - "address": "cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts" - } - }, - "approvals": [] - } - } - ], - "totalCount": 2, - "pageInfo": { - "hasNextPage": false, - "hasPreviousPage": false, - "startCursor": "", - "endCursor": "" - } - } - } - } - ], - "pageInfo": { - "hasNextPage": false, - "hasPreviousPage": false, - "startCursor": "", - "endCursor": "" - }, - "totalCount": 1 - } - } -} -``` - - \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/01-queries/03-tokens-queries.md b/versioned_docs/version-2.x/03-api-reference/01-queries/03-tokens-queries.md deleted file mode 100644 index 0a707ae..0000000 --- a/versioned_docs/version-2.x/03-api-reference/01-queries/03-tokens-queries.md +++ /dev/null @@ -1,317 +0,0 @@ ---- -title: "Tokens" -slug: "tokens" -description: "Query the Enjin API to retrieve detailed information about blockchain tokens, including ownership, metadata, and transaction history." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - -:::tip Core Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.enjin.io/graphql` -::: - -This is a detailed reference guide that explains the most commonly used operations. - -## Get Token - -The `GetToken` query enables you to retrieve detailed information about a specific token from a collection. It requires the collectionId and tokenId parameters to access attributes, account details, and metadata related to the token. - -You can utilize the GetToken query for various purposes, including: - -- Retrieving Token Information: Get detailed information about a specific token, including supply, cap, and freeze status. -- Exploring Accounts: Explore accounts that hold the token, check their balances, and freeze status. -- Context of Collection: Obtain context about the collection to which the token belongs. -- Retrieving Attributes: Access attributes associated with the collection. -- Metadata Retrieval: Retrieve metadata associated with the token, such as name, color, or other descriptive information. - - - -```graphql -query GetToken { - GetToken( - collectionId: 7153 - tokenId: {integer:10} - ) { - tokenId - supply - cap - capSupply - isFrozen - minimumBalance - unitPrice - mintDeposit - attributeCount - nonFungible - metadata - collection { - collectionId - } - attributes { - key - value - } - accounts { - totalCount - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - edges { - cursor - node { - balance - reservedBalance - isFrozen - wallet { - account { - publicKey - address - } - } - approvals { - amount - expiration - wallet { - account { - publicKey - address - } - } - } - namedReserves { - pallet - amount - } - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetToken": { - "tokenId": "10", - "supply": "1", - "cap": "SINGLE_MINT", - "capSupply": null, - "isFrozen": false, - "minimumBalance": "1", - "unitPrice": "10000000000000000", - "mintDeposit": "20000000000000000", - "attributeCount": 0, - "nonFungible": true, - "metadata": null, - "collection": { - "collectionId": "7153" - }, - "attributes": [], - "accounts": { - "totalCount": 1, - "pageInfo": { - "hasNextPage": false, - "hasPreviousPage": false, - "startCursor": "", - "endCursor": "" - }, - "edges": [ - { - "cursor": "eyJpZCI6NzgxOCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "balance": "1", - "reservedBalance": "0", - "isFrozen": false, - "wallet": { - "account": { - "publicKey": "0x985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050", - "address": "cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts" - } - }, - "approvals": [], - "namedReserves": [] - } - } - ] - } - } - } -} -``` - - - -## GetTokens - -The `GetTokens` query allows you to retrieve an array of token data from a collection, making it suitable for batch retrieval and data analysis. You can specify filtering criteria to target specific tokens within the collection. - -You can utilize the `GetTokens` query for various purposes, including: - -- **Bulk Token Retrieval**: Retrieve information about multiple tokens within a collection, making it efficient for displaying token lists or galleries. -- **Filtered Token Retrieval**: Apply filters to obtain specific tokens based on criteria like token ID, collection, or attributes. -- **Marketplace Integration**: Fetch token data for use in a marketplace or trading platform to display available tokens and their details. -- **Data Analysis**: Analyze token data for insights, statistics, or reporting across a collection. -- **User Experience Enhancement**: Enhance the user experience by providing a comprehensive view of tokens within a collection. - - -:::warning Reading Third-Party Tokens -Please note that the Enjin Platform Cloud is set up to show only the collections and tokens that were created via the auth-ed Enjin Platform Cloud account.\ -To get a token that was created elsewhere (via a different Enjin Platform Cloud account / [NFT.io](https://nft.io) / [Enjin Console](https://console.enjin.io) / etc.) the collection must be "Tracked" first, or the query response will return an error.\ -Learn more about tracking a collection in the [Tracking Collections section](/02-guides/01-platform/01-managing-tokens/09-fetching-token-holders.md#tracking-collections). -::: - - - -```graphql -query GetTokens { - GetTokens( - collectionId: 7153 - tokenIds: [{integer:10}] - ) { - edges { - cursor - node { - tokenId - supply - cap - capSupply - isFrozen - minimumBalance - unitPrice - mintDeposit - attributeCount - collection { - collectionId - } - attributes { - key - value - } - accounts { - totalCount - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - edges { - cursor - node { - balance - reservedBalance - isFrozen - wallet { - account { - publicKey - address - } - } - approvals { - amount - expiration - wallet { - account { - publicKey - address - } - } - } - namedReserves { - pallet - amount - } - } - } - } - nonFungible - } - } - totalCount - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - } -} -``` - - -```json -{ - "data": { - "GetTokens": { - "edges": [ - { - "cursor": "eyJjb2xsZWN0aW9uX2lkIjo0OTk4LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", - "node": { - "tokenId": "10", - "supply": "1", - "cap": "SINGLE_MINT", - "capSupply": null, - "isFrozen": false, - "minimumBalance": "1", - "unitPrice": "10000000000000000", - "mintDeposit": "20000000000000000", - "attributeCount": 0, - "collection": { - "collectionId": "7153" - }, - "attributes": [], - "accounts": { - "totalCount": 1, - "pageInfo": { - "hasNextPage": false, - "hasPreviousPage": false, - "startCursor": "", - "endCursor": "" - }, - "edges": [ - { - "cursor": "eyJpZCI6NzgxOCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "balance": "1", - "reservedBalance": "0", - "isFrozen": false, - "wallet": { - "account": { - "publicKey": "0x985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050", - "address": "cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts" - } - }, - "approvals": [], - "namedReserves": [] - } - } - ] - }, - "nonFungible": true - } - } - ], - "totalCount": 1, - "pageInfo": { - "hasNextPage": false, - "hasPreviousPage": false, - "startCursor": "", - "endCursor": "" - } - } - } -} -``` - - \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/01-queries/04-wallets-queries.md b/versioned_docs/version-2.x/03-api-reference/01-queries/04-wallets-queries.md deleted file mode 100644 index fd65501..0000000 --- a/versioned_docs/version-2.x/03-api-reference/01-queries/04-wallets-queries.md +++ /dev/null @@ -1,990 +0,0 @@ ---- -title: "Wallets" -slug: "wallets" -description: "Use the Enjin API to perform wallets queries, including retrieving balances, transaction history, and associated blockchain assets." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - -:::tip Core Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.enjin.io/graphql` -::: - -This is a detailed reference guide that explains the most commonly used operations. - -## GetWallet - -The `GetWallet` query provides detailed information about a specific wallet. This query retrieves various details related to the wallet, including account information, balances, associated collections and tokens, approvals, and transaction history. - - - -```graphql -query GetWallet { - GetWallet( - id: 5 - ) { - id - account { - publicKey - address - } - externalId - managed - network - nonce - balances { - free - reserved - miscFrozen - feeFrozen - } - collectionAccounts { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - accountCount - isFrozen - collection { - collectionId - } - wallet { - account { - publicKey - address - } - } - approvals { - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - } - tokenAccounts { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - balance - reservedBalance - isFrozen - collection { - collectionId - } - wallet { - account { - publicKey - address - } - } - token { - tokenId - } - approvals { - amount - expiration - wallet { - account { - publicKey - address - } - } - } - namedReserves { - pallet - amount - } - } - } - } - collectionAccountApprovals { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - tokenAccountApprovals { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - amount - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - transactions { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - id - transactionId - transactionHash - method - state - encodedData - events { - edges { - cursor - node { - phase - lookUp - moduleId - eventId - params { - type - value - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } - wallet { - account { - publicKey - address - } - } - } - } - } - ownedCollections { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - network - owner { - account { - publicKey - address - } - } - attributes { - key - value - } - accounts { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - accountCount - isFrozen - collection { - collectionId - } - wallet { - account { - publicKey - address - } - } - approvals { - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - } - tokens { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - tokenId - supply - cap - capSupply - isFrozen - minimumBalance - unitPrice - mintDeposit - attributeCount - collection { - collectionId - } - attributes { - key - value - } - accounts { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - balance - } - } - } - metadata - nonFungible - } - } - } - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetWallet": { - "id": 5, - "account": { - "publicKey": "0x1edb67183fcd7846e717b16a37ae4326c7814090c748b5698a3b6a9e946a9f37", - "address": "cxK83gJm3F8LV4Kh1YjA6KaSgJev1RgatHrum4Ap4AFcHSrNe" - }, - "externalId": null, - "managed": false, - "network": "developer", - "nonce": 0, - "balances": { - "free": "0", - "reserved": "0", - "miscFrozen": "0", - "feeFrozen": "0" - }, - "collectionAccounts": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "tokenAccounts": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "collectionAccountApprovals": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "tokenAccountApprovals": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "transactions": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "ownedCollections": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - } - } - } -} -``` - - - -## GetWallets - -The `GetWallets` query allows you to retrieve an array of wallets. You have the option to apply filters based on parameters such as external IDs, verification IDs, or account addresses to narrow down the results. This query is useful for applications that need to access and display user wallet information, including transaction history and asset balances. - -:::warning This query might return items in multiple pages using Connections -To learn how to use GraphQL cursors for pagination, head to [Using the API → Pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination). -::: - - - -```graphql -query GetWallets { - GetWallets( - ids: [5] - ) { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - id - account { - publicKey - address - } - externalId - managed - network - nonce - balances { - free - reserved - miscFrozen - feeFrozen - } - collectionAccounts { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - accountCount - isFrozen - collection { - collectionId - } - wallet { - account { - publicKey - address - } - } - approvals { - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - } - tokenAccounts { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - balance - reservedBalance - isFrozen - collection { - collectionId - } - wallet { - account { - publicKey - address - } - } - token { - tokenId - } - approvals { - amount - expiration - wallet { - account { - publicKey - address - } - } - } - namedReserves { - pallet - amount - } - } - } - } - collectionAccountApprovals { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - tokenAccountApprovals { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - amount - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - transactions { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - id - transactionId - transactionHash - method - state - encodedData - events { - edges { - cursor - node { - phase - lookUp - moduleId - eventId - params { - type - value - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } - wallet { - account { - publicKey - address - } - } - } - } - } - ownedCollections { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - network - owner { - account { - publicKey - address - } - } - attributes { - key - value - } - accounts { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - accountCount - isFrozen - collection { - collectionId - } - wallet { - account { - publicKey - address - } - } - approvals { - expiration - wallet { - account { - publicKey - address - } - } - } - } - } - } - tokens { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - tokenId - supply - cap - capSupply - isFrozen - minimumBalance - unitPrice - mintDeposit - attributeCount - collection { - collectionId - } - attributes { - key - value - } - accounts { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - balance - } - } - } - metadata - nonFungible - } - } - } - } - } - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetWallets": { - "totalCount": 1, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [ - { - "cursor": "eyJpZCI6NSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 5, - "account": { - "publicKey": "0x1edb67183fcd7846e717b16a37ae4326c7814090c748b5698a3b6a9e946a9f37", - "address": "cxK83gJm3F8LV4Kh1YjA6KaSgJev1RgatHrum4Ap4AFcHSrNe" - }, - "externalId": null, - "managed": false, - "network": "developer", - "nonce": 0, - "balances": { - "free": "0", - "reserved": "0", - "miscFrozen": "0", - "feeFrozen": "0" - }, - "collectionAccounts": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "tokenAccounts": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "collectionAccountApprovals": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "tokenAccountApprovals": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "transactions": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - }, - "ownedCollections": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - } - } - } - ] - } - } -} -``` - - - -## GetAccountVerified - -The `GetAccountVerified` query allows you to check the verification status of a specific account. It is used to determine whether an account is verified. The query takes a `verificationId` as a parameter, which serves as a unique identifier for the verification process. -The response will include whether the account is verified (`true` or `false`) and additional account information for reference. - - - -```graphql -query GetAccountVerified { - GetAccountVerified( - verificationId: "0x684768963b832de4f593da456ed3d099c2b8c580383492d3b0e735c4d1dfe740" - ) { - verified - account { - publicKey - address - } - } -} -``` - - -```json -{ - "data": { - "GetAccountVerified": { - "verified": true, - "account": { - "publicKey": "0x8b4cd0e710d3cd14b3a407cac1326dc6c8ac68b4ff22f8d239cbd460d831d608", - "address": "cxMaEYKfjJQrQQZ4cmXUjRqfPNRkRUSgsESMiEeyP6pzsBm4V" - } - } - } -} -``` - - - -## RequestAccount - -The `RequestAccount` query is designed for generating a QR code that users can scan to provide their wallet account information. It facilitates user authentication and linking wallet accounts to a service or application. The generated QR code contains encoded data that, when scanned, triggers a specific action, such as authentication or the transfer of wallet details. - - - -```graphql -query RequestAccount { - RequestAccount( - callback: "https://enjin.io" - ) { - qrCode - verificationId - } -} -``` - - -```json -{ - "data": { - "RequestAccount": { - "qrCode": "https://chart.googleapis.com/chart?chs=512x512&cht=qr&chl=https://deeplink.wallet.enjin.io/proof/0xa144ac5ef19bf4e3523cca27d9f998781c64692ea68843602da0dc95f6c4aa70:A2010LJAJ:aHR0cHM6Ly9lbmppbi5pbw==", - "verificationId": "0xa144ac5ef19bf4e3523cca27d9f998781c64692ea68843602da0dc95f6c4aa70" - } - } -} -``` - - - -## VerifyMessage - -The `VerifyMessage` query is used to verify the authenticity of a message by confirming that it was signed with a specific private key corresponding to the provided public key. This query is essential for ensuring the integrity and origin of messages in a secure communication or authentication process. - - - -```graphql -query VerifyMessage { - VerifyMessage( - message: "0x74657374" - signature: "0xbe32fc5fbe033ebb2dd94bcf0d06a52299042592006b0956cfd7606dea015b60f834d3b299172ad098e841efa3a61e9fb8e37a8912f85821eaf3df64f0e49083" - publicKey: "0xb0e3d2a901fe662e3f693704a599d4ec06da13b213f46e9ba479ba8e95dbfa61" - cryptoSignatureType: SR25519 - ) -} -``` - - -```json -{ - "data": { - "VerifyMessage": true - } -} -``` - - - -## GetPendingWallets - -The `GetPendingWallets` query is used to retrieve an array of wallet accounts that have not yet undergone the verification process. This query is particularly useful for administrators and account management systems to identify and handle pending wallet accounts that require verification. - - - -```graphql -query GetPendingWallets { - GetPendingWallets { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - edges { - cursor - node { - id - account { - publicKey - address - } - externalId - managed - network - } - } - } -} -``` - - -```json -{ - "data": { - "GetPendingWallets": { - "totalCount": 0, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasNextPage": false, - "hasPreviousPage": false - }, - "edges": [] - } - } -} -``` - - diff --git a/versioned_docs/version-2.x/03-api-reference/01-queries/05-fuel-tank-queries.md b/versioned_docs/version-2.x/03-api-reference/01-queries/05-fuel-tank-queries.md deleted file mode 100644 index 8a9e7d8..0000000 --- a/versioned_docs/version-2.x/03-api-reference/01-queries/05-fuel-tank-queries.md +++ /dev/null @@ -1,283 +0,0 @@ ---- -title: "Fuel Tanks" -slug: "fuel-tanks" -description: "Query the Enjin API for fuel tanks data, which helps cover transaction costs for users within the blockchain ecosystem." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - -:::tip Fuel Tanks Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql/fuel-tanks` -- **Mainnet:** `http://platform.enjin.io/graphql/fuel-tanks` -::: - -This is a detailed reference guide that explains the most commonly used operations. - -## GetAccounts - -The `GetAccounts` query allows you to retrieve a comprehensive list of account details associated with a specific fuel tank in the Enjin API. Fuel tanks are a feature used to subsidize transaction fees on the Enjin Blockchain. This query provides a detailed overview of all accounts participating in the specified fuel tank. - - - -```graphql -query GetAccounts { - GetAccounts(tankId: "cxNC9WmJa55k3EoXS3Fm4Bc69NaMGuY7qarchqPPn6jcWMA92") { - totalCount - pageInfo { - startCursor - endCursor - hasPreviousPage - hasNextPage - } - edges { - node { - id - externalId - account { - publicKey - address - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetFuelTanks": { - "totalCount": 1, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasPreviousPage": false, - "hasNextPage": false - }, - "edges": [ - { - "node": { - "name": "lhjb4xu2", - "providesDeposit": false, - "tankId": { - "publicKey": "0xa6b0ba89304439830fa19e0523ad8afa4180bb0960d35a80b7423836dd7b1276", - "address": "cxNC9WmJa55k3EoXS3Fm4Bc69NaMGuY7qarchqPPn6jcWMA92" - }, - "owner": { - "account": { - "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", - "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" - } - }, - "accounts": [ - { - "id": 8177, - "account": { - "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", - "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" - }, - "externalId": null - } - ], - "accountRules": [], - "dispatchRules": [] - } - } - ] - } - } -} -``` - - - -## GetFuelTank - -The `GetFuelTank` query allows you to retrieve detailed information about a specific fuel tank within the Enjin API. Fuel tanks are utilized for subsidizing transaction fees on the Enjin Blockchain. This query provides an extensive overview of the fuel tank's configuration, ownership, and associated rules. - - - -```graphql -query GetFuelTank { - GetFuelTank(tankId: "cxNC9WmJa55k3EoXS3Fm4Bc69NaMGuY7qarchqPPn6jcWMA92") { - name - providesDeposit - tankId { - publicKey - address - } - owner { - account { - publicKey - address - } - } - accounts { - id - account { - publicKey - address - } - externalId - } - accountRules { - rule - value - } - dispatchRules { - rule - value - isFrozen - } - } -} -``` - - -```json -{ - "data": { - "GetFuelTank": { - "name": "lhjb4xu2", - "providesDeposit": false, - "tankId": { - "publicKey": "0xa6b0ba89304439830fa19e0523ad8afa4180bb0960d35a80b7423836dd7b1276", - "address": "cxNC9WmJa55k3EoXS3Fm4Bc69NaMGuY7qarchqPPn6jcWMA92" - }, - "owner": { - "account": { - "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", - "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" - } - }, - "accounts": [ - { - "id": 8177, - "account": { - "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", - "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" - }, - "externalId": null - } - ], - "accountRules": [], - "dispatchRules": [] - } - } -} -``` - - - -## GetFuelTanks - -The `GetFuelTanks` query allows you to retrieve detailed information about multiple fuel tanks within the Enjin API simultaneously. This query is particularly useful when you need to obtain an overview or compare details of various fuel tanks within a system. - -:::warning This query might return items in multiple pages using Connections -To learn how to use GraphQL cursors for pagination, head to [Using the API → Pagination](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#pagination). -::: - - - -```graphql -query GetFuelTanks{ - GetFuelTanks(tankIds: ["0xa6b0ba89304439830fa19e0523ad8afa4180bb0960d35a80b7423836dd7b1276"]) { - totalCount - pageInfo { - startCursor - endCursor - hasPreviousPage - hasNextPage - } - edges { - node { - name - providesDeposit - tankId { - publicKey - address - } - owner { - account { - publicKey - address - } - } - accounts { - id - account { - publicKey - address - } - externalId - } - accountRules { - rule - value - } - dispatchRules { - rule - value - isFrozen - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetFuelTanks": { - "totalCount": 1, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasPreviousPage": false, - "hasNextPage": false - }, - "edges": [ - { - "node": { - "name": "lhjb4xu2", - "providesDeposit": false, - "tankId": { - "publicKey": "0xa6b0ba89304439830fa19e0523ad8afa4180bb0960d35a80b7423836dd7b1276", - "address": "cxNC9WmJa55k3EoXS3Fm4Bc69NaMGuY7qarchqPPn6jcWMA92" - }, - "owner": { - "account": { - "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", - "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" - } - }, - "accounts": [ - { - "id": 8177, - "account": { - "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", - "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" - }, - "externalId": null - } - ], - "accountRules": [], - "dispatchRules": [] - } - } - ] - } - } -} -``` - - \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/01-queries/06-marketplace-queries.md b/versioned_docs/version-2.x/03-api-reference/01-queries/06-marketplace-queries.md deleted file mode 100644 index f18084f..0000000 --- a/versioned_docs/version-2.x/03-api-reference/01-queries/06-marketplace-queries.md +++ /dev/null @@ -1,593 +0,0 @@ ---- -title: "Marketplace" -slug: "marketplace" -description: "Query the Enjin API for marketplace data, including listing information, token trades, and transaction details." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - -:::tip Marketplace Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql/marketplace` -- **Mainnet:** `http://platform.enjin.io/graphql/marketplace` -::: - -This is a detailed reference guide that explains the most commonly used operations. - -## GetBid - -The `GetBid` query allows you to retrieve detailed information about a specific bid placed on a market listing. It's a valuable tool for querying bid-related data, including bid price, bidder details, and the associated market listing. - - -```graphql -query GetBid { - GetBid(id: 1) { - id - price - height - bidder { - account { - publicKey - address - } - } - listing { - listingId - } - } -} -``` - - -```json -{ - "data": { - "GetBid": { - "id": 1, - "price": "1000000000000000000", - "height": 239058, - "bidder": { - "account": { - "publicKey": "0xb4664455021025f4944c1bc4af8a2830317f0765dc624778345dea09e89a526a", - "address": "cxNW84hdDPEr8rU8oUbqNDaCmVX6SxH86mFufTf6HiNbZhi2F" - } - }, - "listing": { - "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20" - } - } - } -} -``` - - - -## GetBids - -The `GetBids` query is used to retrieve a list of bids for one or more market listings based on specified listing IDs. Unlike the `GetBid` query, which retrieves details of a single bid, `GetBids` can return multiple bids and includes pagination information. - - - -```graphql -query GetBids { - GetBids(listingIds: ["0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20"]) { - totalCount - pageInfo { - startCursor - endCursor - hasPreviousPage - hasNextPage - } - edges { - node { - id - price - height - bidder { - account { - publicKey - address - } - } - listing { - listingId - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetBids": { - "totalCount": 1, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasPreviousPage": false, - "hasNextPage": false - }, - "edges": [ - { - "node": { - "id": 1, - "price": "1000000000000000000", - "height": 239058, - "bidder": { - "account": { - "publicKey": "0xb4664455021025f4944c1bc4af8a2830317f0765dc624778345dea09e89a526a", - "address": "cxNW84hdDPEr8rU8oUbqNDaCmVX6SxH86mFufTf6HiNbZhi2F" - } - }, - "listing": { - "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20" - } - } - } - ] - } - } -} -``` - - - -## GetListing - -The `GetListing` query allows you to retrieve detailed information about a single market listing using its unique listing ID. This query provides a comprehensive overview of the listing's attributes, its state, sales history, bid history, and more. - - - -```graphql -query GetListing { - GetListing(listingId: "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20") { - listingId - makeAssetId { - collectionId - tokenId - } - takeAssetId { - collectionId - tokenId - } - amount - price - minTakeValue - feeSide - creationBlock - deposit - salt - state { - ... on FixedPriceState { - amountFilled - type - } - ... on AuctionState { - type - } - } - data { - ... on FixedPriceData { - type - } - ... on AuctionData { - type - startBlock - endBlock - } - } - seller { - account { - publicKey - address - } - } - sales { - edges { - node { - amount - price - bidder { - account { - publicKey - address - } - } - } - } - } - bids { - edges { - node { - price - bidder { - account { - publicKey - address - } - } - height - } - } - } - states { - state - height - } - } -} -``` - - -```json -{ - "data": { - "GetListing": { - "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20", - "makeAssetId": { - "collectionId": "4919", - "tokenId": "0" - }, - "takeAssetId": { - "collectionId": "0", - "tokenId": "0" - }, - "amount": "1", - "price": "1000000000000000000", - "minTakeValue": "975000000000000000", - "feeSide": "TAKE_FEE", - "creationBlock": 239058, - "deposit": "2025700000000000000", - "salt": "73616c74313233", - "state": { - "amountFilled": null, - "type": "FIXED_PRICE" - }, - "data": { - "type": "FIXED_PRICE" - }, - "seller": { - "account": { - "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", - "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" - } - }, - "sales": { - "edges": [] - }, - "bids": { - "edges": [ - { - "node": { - "price": "1000000000000000000", - "bidder": { - "account": { - "publicKey": "0xb4664455021025f4944c1bc4af8a2830317f0765dc624778345dea09e89a526a", - "address": "cxNW84hdDPEr8rU8oUbqNDaCmVX6SxH86mFufTf6HiNbZhi2F" - } - }, - "height": 239058 - } - } - ] - }, - "states": [ - { - "state": "ACTIVE", - "height": 239058 - } - ] - } - } -} -``` - - - -## GetListings - -The `GetListings` query is used to retrieve information about multiple market listings from the Enjin Blockchain marketplace. Unlike the `GetListing` query, which fetches data for a single listing, `GetListings` is designed to handle multiple listings at once, making it suitable for scenarios where you need to fetch details for multiple listings in a single request. - - - -```graphql -query GetListings { - GetListings( - listingIds: ["0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20"] - ) { - totalCount - pageInfo { - startCursor - endCursor - hasPreviousPage - hasNextPage - } - edges { - node { - listingId - makeAssetId { - collectionId - tokenId - } - takeAssetId { - collectionId - tokenId - } - amount - price - minTakeValue - feeSide - creationBlock - deposit - salt - state { - ... on FixedPriceState { - amountFilled - type - } - ... on AuctionState { - type - } - } - data { - ... on FixedPriceData { - type - } - ... on AuctionData { - type - startBlock - endBlock - } - } - seller { - account { - publicKey - address - } - } - sales { - edges { - node { - amount - price - bidder { - account { - publicKey - address - } - } - } - } - } - bids { - edges { - node { - price - bidder { - account { - publicKey - address - } - } - height - } - } - } - states { - state - height - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetListings": { - "totalCount": 1, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasPreviousPage": false, - "hasNextPage": false - }, - "edges": [ - { - "node": { - "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20", - "makeAssetId": { - "collectionId": "4919", - "tokenId": "0" - }, - "takeAssetId": { - "collectionId": "0", - "tokenId": "0" - }, - "amount": "1", - "price": "1000000000000000000", - "minTakeValue": "975000000000000000", - "feeSide": "TAKE_FEE", - "creationBlock": 239058, - "deposit": "2025700000000000000", - "salt": "73616c74313233", - "state": { - "amountFilled": null, - "type": "FIXED_PRICE" - }, - "data": { - "type": "FIXED_PRICE" - }, - "seller": { - "account": { - "publicKey": "0x087c3fdc6566230578362759d99e42ed300f5560c305262843b2c61aa2f1d11e", - "address": "cxJciMkfdBqR8C9ftA8qmgzP9bAQNzMzXZmwUDZ2qW5mFVgvm" - } - }, - "sales": { - "edges": [] - }, - "bids": { - "edges": [ - { - "node": { - "price": "1000000000000000000", - "bidder": { - "account": { - "publicKey": "0xb4664455021025f4944c1bc4af8a2830317f0765dc624778345dea09e89a526a", - "address": "cxNW84hdDPEr8rU8oUbqNDaCmVX6SxH86mFufTf6HiNbZhi2F" - } - }, - "height": 239058 - } - } - ] - }, - "states": [ - { - "state": "ACTIVE", - "height": 239058 - } - ] - } - } - ] - } - } -} -``` - - - -## GetSale - -The `GetSale` query is used to retrieve details about a specific sale transaction that has occurred on the Enjin Blockchain marketplace. This query requires the sale ID as input and returns information about the sale, including the sale ID, price, quantity, bidder information, and the associated market listing. - - - -```graphql -query GetSale { - GetSale(id: 1) { - id - price - amount - bidder { - account { - publicKey - address - } - } - listing { - listingId - } - } -} -``` - - -```json -{ - "data": { - "GetSale": { - "id": 1, - "price": "1000000", - "amount": "1", - "bidder": { - "account": { - "publicKey": "0x6a03b1a3d40d7e344dfb27157931b14b59fe2ff11d7352353321fe400e956802", - "address": "cxLpbEojWougf2cE6onB6PuK9SsfmKqNJKNo9tTsqirxFbuN5" - } - }, - "listing": { - "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20" - } - } - } -} -``` - - - -## GetSales - -The `GetSales` query is used to retrieve information about completed sale transactions that have occurred on a marketplace. This query allows you to fetch details of multiple sales associated with specific market listings. - - - -```graphql -query GetSales { - GetSales(listingIds: ["0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20"]) { - totalCount - pageInfo { - startCursor - endCursor - hasPreviousPage - hasNextPage - } - edges { - node { - id - price - amount - bidder { - account { - publicKey - address - } - } - listing { - listingId - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetSales": { - "totalCount": 1, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasPreviousPage": false, - "hasNextPage": false - }, - "edges": [ - { - "node": { - "id": 1, - "price": "1000000", - "amount": "1", - "bidder": { - "account": { - "publicKey": "0x6a03b1a3d40d7e344dfb27157931b14b59fe2ff11d7352353321fe400e956802", - "address": "cxLpbEojWougf2cE6onB6PuK9SsfmKqNJKNo9tTsqirxFbuN5" - } - }, - "listing": { - "listingId": "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20" - } - } - } - ] - } - } -} -``` - - \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/01-queries/07-beam-queries.md b/versioned_docs/version-2.x/03-api-reference/01-queries/07-beam-queries.md deleted file mode 100644 index b03aa05..0000000 --- a/versioned_docs/version-2.x/03-api-reference/01-queries/07-beam-queries.md +++ /dev/null @@ -1,613 +0,0 @@ ---- -title: "Beam" -slug: "beam" -description: "Query the Enjin API for Beam data, including the status and details of QR code activations for blockchain assets distribution." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - -:::tip Beam Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql/beam` -- **Mainnet:** `http://platform.enjin.io/graphql/beam` -::: - -This is a detailed reference guide that explains the most commonly used operations. - -## GetBeam - -The `GetBeam` query retrieves detailed information about a specific . A Beam is a feature provided by Enjin that allows users to claim blockchain assets via QR codes. This query is essential for applications that need to access and display information about a particular Beam or track its usage. - - - -```graphql -query GetBeam { - GetBeam(code: "0dd3694f2c4599179f08686539cd73dc") { - id - code - name - description - image - start - end - isClaimable - claimsRemaining - qr { - url - payload - } - message { - walletPublicKey - message - } - collection { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - } - } -} - -``` - - -```json -{ - "data": { - "GetBeam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc", - "name": "236", - "description": "This is the most awesome Beam ever.", - "image": "https://en.wikipedia.org/wiki/Firefox#/media/File:Firefox_logo,_2019.svg", - "start": "2022-11-14T00:00:00+00:00", - "end": "2028-11-14T00:00:00+00:00", - "isClaimable": true, - "claimsRemaining": 10, - "qr": { - "url": "https://platform.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc", - "payload": "https://platform.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc" - }, - "message": null, - "collection": null - } - } -} -``` - - - -## GetBeams - -The `GetBeams` query is used to retrieve an array of beam details, optionally filtered by codes or names. This query is particularly useful for applications that need to fetch information about multiple Beams at once, making it easier to manage and display Beam data. - - - -```graphql -query GetBeams { - GetBeams(codes: ["0dd3694f2c4599179f08686539cd73dc"]) { - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - edges { - node { - id - code - name - description - image - start - end - isClaimable - qr { - url - payload - } - collection { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - } - claims { - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - edges { - node { - id - claimedAt - claimStatus - quantity - code - wallet { - account { - publicKey - address - } - } - collection { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - } - } - } - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetBeams": { - "totalCount": 1, - "pageInfo": { - "hasPreviousPage": false, - "hasNextPage": false, - "startCursor": "", - "endCursor": "" - }, - "edges": [ - { - "node": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc", - "name": "236", - "description": "This is the most awesome Beam ever.", - "image": "https://en.wikipedia.org/wiki/Firefox#/media/File:Firefox_logo,_2019.svg", - "start": "2022-11-14T00:00:00+00:00", - "end": "2028-11-14T00:00:00+00:00", - "isClaimable": true, - "qr": { - "url": "https://platform.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc", - "payload": "https://platform.enjin.io/claim/0dd3694f2c4599179f08686539cd73dc" - }, - "collection": null, - "claims": { - "totalCount": 10, - "pageInfo": { - "hasPreviousPage": false, - "hasNextPage": false, - "startCursor": "", - "endCursor": "" - }, - "edges": [ - { - "node": { - "id": 1, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6IllGcEg0L0lRL0x5c1BQeWIyVXdCL0E9PSIsInZhbHVlIjoiVmhtcTRqcUhoeFBPTGQ2QTNWOTQ5aWhOR3poVlJrOFo0QTVHeUpVSUZWcTFwaGR1YW1KMmN5YWJLZ2xKbW1WSSIsIm1hYyI6ImRiMGIyMTAyNjEwOTNkNWUwNTUxZWVkMDhlZGEyZjQ2YmQwMGY5MzhhYjA5ZjBmZmE4MDk4NmNiYTkzM2FiNWQiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 2, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6ImllVllKclNObjk4QXdtZnRjVitLWmc9PSIsInZhbHVlIjoiQlozZzVQSHZWOTZibWxGSkkzcWxBMVNiMkFaZGcxcERGNitBOE1sMFVwZFJsdkE5OXBMVlNlRVZxOGN4eGZaRyIsIm1hYyI6IjI1NzQ5ZjRkOGMzMzBjMWRkNjY1YWZhYjllYjliYmE0OThmYmQ1MGFiMjhhYjhiNTA0ZDlkYWM2OTU5YzJmN2YiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 3, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6IjZYdUVWeTYwK0czRjM2THNHNVBJWFE9PSIsInZhbHVlIjoialhHbS93YkhnNldJa2JHeDFBMCt2V3ZLRDZkVi9INXgvdG1sOGxhSTJwWk12RE1HMEN2ZFZyY2d3dzRDQ0loNCIsIm1hYyI6IjVjOGE0ZGEyYzUzYWQzODgwZDJkNmNlZjM4YjY0YmZmOTcwNTVhNGIyNzUzYWNlYjMxOWNiNGE5OWZhY2NiNmYiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 4, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6Ik9TeDE3ZGMxODFHdXRwUTM4QXp3OUE9PSIsInZhbHVlIjoiaC9mUGtjTkd5RjNFdEk4WGFnNW5QOWJhVFVBR3ZrOXE3bTMrTm9GT01SVVV5NVpVRSt2RGtnQndnTld3TFRlZyIsIm1hYyI6IjdhNjgzYTJhMWQ2NzZlZWM1NTIyZWVkNjFkNzNjNzNlN2JhNmMyZTAyYzhmYTE2M2M1NzNlMTAxNTNlMDI3OGQiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 5, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6IlhYVVF0c0l0ZDNzS2g3a0hLU3JaeVE9PSIsInZhbHVlIjoiWWltZjlvMjJ1M0psRHJPUG1QRmUvVlZnR2RiREQyTWxYVU9pSUtNb2N2MWxqbzFmTVYxM0xiV3cxZWJ4NFg4TyIsIm1hYyI6IjdlOTExYTg5OTIwMDMxMTZkYmJlNGYwZjU5MjhkOWJlMzgyZWVhYzcyMDEwYzRkNzk5Zjk2OWU5YjZjODExNmYiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 6, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6IlVlUjBKeUhrZ092clBqMDVNaTNwOXc9PSIsInZhbHVlIjoiOUNhcUxaQkVDVlZFdnlBM3ZtTUJmWnl0bjN6YVNuUmRXbmoxWXZiOFpZOUFqaW9Od2Z0ZGdWMjF0S2llRHZEUyIsIm1hYyI6Ijg0YjY1ZWEzZGFjOWE5NjI2OTIwMjExNzc2NTNhZWZiMDI3OGQwZGQyZDEzYWJhMjg2OGExZWQyMzJiMmJlM2EiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 7, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6InAxU1I4QzlXcGp2RXRuSENCTkVzcnc9PSIsInZhbHVlIjoiL2J2a0UyK0trd0pJMVJ0QVI0NU9TanFuZmVCcUY3dG5TNW50YURzbXdwUkhBSVIyQTlnQnZHK0trTjh1am9pbiIsIm1hYyI6ImMzYzVkZDZmOTQ2OWRiODBmOTMwMWJkYzZhZTE0ZTkwZTczOGFlZjJkMmYzOTQzZjA1ZGMxNjExMmU5MjRiYjQiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 8, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6InVaRWlMYk1HSjdkZFRhWVlGQlRMK3c9PSIsInZhbHVlIjoidUJjTWpPODZRMThNeUF1T0hHaWJlUTB0a0thTXl5VnZFSHN4UjYzQk5uZ1NQR0lrNWR1NlY1VG5sU2cyZ0R6diIsIm1hYyI6ImZiZjhkY2JhOTY3MmUwZGI2YzYyZmQ4MGFkYmVjMzI2NzRiYWZmOWI4ZjgzMjNkYTRlZTc5MmEyNzZmN2E0MzciLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 9, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6Ik14U2ZndlJiLzdsQktMckdQTGQxeHc9PSIsInZhbHVlIjoiUDBEWGNYcDk1ZExlZlMzNzRPWEdFeFliYmIwWWIxUElzaEpHSkQxb05xN2EyVVVucnd2dWQzTC9Ic3JzYnlSeSIsIm1hYyI6ImY4ODI0OGU0Mzc3NjVkMzZlODljYTI2YzhkNDU3N2MyMmNiZTM0YjZkMWQ3YjU2NjE0NmZlZjkyYjk0NjllOGQiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - }, - { - "node": { - "id": 10, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "code": "eyJpdiI6InhiYWZqWlM3YTRhc01EYXZaR2pwRFE9PSIsInZhbHVlIjoiZXlwVytJbWNRT29VOThrR28wWldBa1l3Y3ZPMW5oUmdwUE02Snh1M2FDek4wOXpsS1FEVEFjNU9GV2psNWZCMyIsIm1hYyI6ImI3NTU3YTJjNWQzODQyNjllOGJmZTA5MjBlMjBhYTI3ZTA1Yjg1MmYzN2RiOWQ2MDJjMGRlMmY3Y2YxMDk2ODkiLCJ0YWciOiIifQ==", - "wallet": null, - "collection": null - } - } - ] - } - } - } - ] - } - } -} -``` - - - -## GetClaims - -The `GetClaims` query is used to retrieve an array of claim details from a system, and it allows for optional filtering based on claim IDs, codes, accounts, or states. This query is typically used to track and manage claims associated with specific assets or codes in a system. - - - -```graphql -query GetClaims { - GetClaims( - codes: ["0dd3694f2c4599179f08686539cd73dc"] - ) { - edges { - cursor - node { - id - claimedAt - claimStatus - quantity - wallet { - account { - publicKey - address - } - } - collection { - collectionId - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - network - } - beam { - id - code - } - } - } - totalCount - pageInfo { - startCursor - endCursor - hasPreviousPage - hasNextPage - } - } -} -``` - - -```json -{ - "data": { - "GetClaims": { - "edges": [ - { - "cursor": "eyJpZCI6MTAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "id": 10, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6OSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 9, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6OCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 8, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6NywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 7, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6NiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 6, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6NSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 5, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6NCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 4, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6MywiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 3, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 2, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - }, - { - "cursor": "eyJpZCI6MSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", - "node": { - "id": 1, - "claimedAt": null, - "claimStatus": null, - "quantity": 10, - "wallet": null, - "collection": null, - "beam": { - "id": 1, - "code": "0dd3694f2c4599179f08686539cd73dc" - } - } - } - ], - "totalCount": 10, - "pageInfo": { - "startCursor": "", - "endCursor": "", - "hasPreviousPage": false, - "hasNextPage": false - } - } - } -} -``` - - - -## GetSingleUseCodes - -The `GetClaims` query retrieves an array of claim details, optionally filtered by claim IDs, claim codes, accounts, or claim states. It is designed to provide information about claims associated with a specific code. - - - -```graphql -query GetSingleUseCodes { - GetSingleUseCodes(code: "7cd390ab3d56c42b818d0a1aa2221c8f") { - edges { - cursor - node { - code - qr { - url - payload - } - } - } - totalCount - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - } -} -``` - - -```json -{ - "data": { - "GetSingleUseCodes": { - "edges": [ - { - "cursor": "eyJpZCI6ODAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "code": "eyJpdiI6IlNFM2tRZzgxaU5pZi9hVXBVZTBCeVE9PSIsInZhbHVlIjoicDdsSmlCU1JHNnd6QUtHU1FRV2pNQ2twaDNPT2YreFVrK3Jrb1VXL0NPc2tOeFBVdTZFM2J1Ykp5NTlRNlNwZXg0d1JTbHo1N1BHMDJqaG11WDhDTGIxRVlVenBoTWVwT3ZrL3RwR2pZTzA9IiwibWFjIjoiYWI3ZjdhY2RmMzE2NGM1MjAxMWVmYTk2ZWNjNWQyNTY4MWQ4NGZhY2ZhN2M5Y2FmMmM0MzU5MDM1N2Q1NDFiYSIsInRhZyI6IiJ9", - "qr": { - "url": "https://platform.enjin.io/claim/eyJpdiI6ImVMNTEybWEwSnZ6WlF3QjUvQW15Wmc9PSIsInZhbHVlIjoiVklOcCtCYVpGTG1aRjQyY0FZb3RmS1JSd1JGT0dka05rT1NQUnZoMXZ4UGsxVklzVmNoejhEUDQ5TjVMc0picjR4MmEzQUJTaWMrUVYxb21yMExZRWVjUGp1T2Rnb0owMHcwWmxXWCtGMnM9IiwibWFjIjoiMjUyNzdlODkyMTRkZmViM2M1OWY5MWNlYjgxYjUzOGE3NTA4NmU0NjY3MjIwOGY0ZDkxNTg3ZTJjOWM5NjUxMSIsInRhZyI6IiJ9", - "payload": "https://platform.enjin.io/claim/eyJpdiI6IkJJWVNYb3lFVGRpaUNEemNJQ20xamc9PSIsInZhbHVlIjoic1ZXMzNGN1FnbWVRc1Z2S2IrYThIT0Rpd2JpeGhza2wxSG1FSlVqTUhzN3UrYmozaXNnZ2dSTU5HTmluZ2laY3JWNUcyemcyaDJZT1VGdGx6SjgxdGVwdUIvN1JhRHRVSnd2RFRYZVhOOUk9IiwibWFjIjoiNzllYmQ1MjFlODBmMWEzOTdjOTc0YWM0YjQ4M2ExZmEyOWZhNzQyY2RlNGJjMzcxNDdkOTQxOWJlYmYyMmUwMSIsInRhZyI6IiJ9" - } - } - }, - { - "cursor": "eyJpZCI6NzEsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", - "node": { - "code": "eyJpdiI6Imp5R0xzNC9aVWZlT090SGVsL3RyVlE9PSIsInZhbHVlIjoiemJiWmsrZEl2TXVzc2R1c2svalhIRjdGVUFXRThPOTg2RnZJVCtMNEE1UENxN3FLeURhL1J4UXhBakduekduRmxRQTJwdmRCeXZqQi9rbVVyVnRmK2V4c2xDZU9jUW15N0xNSkRiUyt2ZlE9IiwibWFjIjoiNTZlOGQ4NjdmNDNmOTQ3NWYwM2M2MzRlMGRmODlmNmQwZmM2ZDllNjdmMTk4ZTA0ZWJiYTliZTZkZTJhOTM3MyIsInRhZyI6IiJ9", - "qr": { - "url": "https://platform.enjin.io/claim/eyJpdiI6IlhtTHdEdlM5bXlQNnVEaGZpejkrR3c9PSIsInZhbHVlIjoidlh3dVhuTVhOVEQvcElBM0drMVhBQ2Z1SFdob3BtNmJnYVQwWExoVmlUYWdPTUhkUzJ1QS9JL25yeHVPSnBQb2VyM3lJWXd3aEMySVd1MjkwellTdW50UVl1YTJRdmNCV3lzdXVLaE9Wckk9IiwibWFjIjoiOGNhNmY1MzZlZWRjMmE0ZjMwYjQ0ODY0Nzg5MTk5MDU4OTAwYmZhYjYzODI4OGQ3ZDIyNTQ5MjA2Zjc2MTczYiIsInRhZyI6IiJ9", - "payload": "https://platform.enjin.io/claim/eyJpdiI6IkN4OHZzUDdGc1AwUG9wdXpQSDVGYXc9PSIsInZhbHVlIjoiOTJJRHQ3VjdoZHR0WVpqajVoSGZIZFNoZHJtVE1iRjR0ZndLQXRQYVE1TS8zaGd1WEZDdDQrckNzUXExdnFvZDVVcE5PMVU1bklFRWhqSnVXVi96WVB4RE5CMk1qTFhHYVIvVWdscFljUTA9IiwibWFjIjoiYWIyY2U4YmIzMjJmNjY0ZmE2NzZjMTZhYTE0YjJlMTEwNDQxMjJlY2Y3NzZlYzZkMDNhMGI3NGQ2MDEyYTZlZSIsInRhZyI6IiJ9" - } - } - } - ], - "totalCount": 10, - "pageInfo": { - "hasPreviousPage": false, - "hasNextPage": false, - "startCursor": "", - "endCursor": "" - } - } - } -} -``` - - \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/01-queries/_category_.json b/versioned_docs/version-2.x/03-api-reference/01-queries/_category_.json deleted file mode 100644 index fa343b5..0000000 --- a/versioned_docs/version-2.x/03-api-reference/01-queries/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Queries (Read)" -} - \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/02-mutations/01-transaction-mutations.md b/versioned_docs/version-2.x/03-api-reference/02-mutations/01-transaction-mutations.md deleted file mode 100644 index f1c3b72..0000000 --- a/versioned_docs/version-2.x/03-api-reference/02-mutations/01-transaction-mutations.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: "Transactions" -slug: "transactions" -description: "Use the Enjin API to perform transactions mutations, enabling the creation and modification of blockchain transactions, including asset transfers." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - -:::tip Core Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.enjin.io/graphql` -::: - -This is an overview of some of the most commonly used operations in the Enjin Platform Schema. - -## AcknowledgeEvents - -The `AcknowledgeEvents` mutation allows you to confirm the processing of specific events by acknowledging them and removing them from the cache. This helps maintain cache integrity and ensures efficient system operation by eliminating unnecessary data. - - - -```graphql -mutation AcknowledgeEvents{ - AcknowledgeEvents( - uuids: ["25f506ef-92a1-40a9-b034-60f9ab5045da"] - ) -} -``` - - -```json -{ - "data": { - "AcknowledgeEvents": true - } -} -``` - - - -## UpdateTransaction - -The `UpdateTransaction` mutation enables you to update an existing transaction with new information. This is useful for maintaining accurate transaction records and tracking the lifecycle of transactions. - - - -```graphql -mutation UpdateTransaction{ - UpdateTransaction( - id: 7 - signedAtBlock: 320100 - ) -} -``` - - -```json -{ - "data": { - "UpdateTransaction": true - } -} -``` - - - -## RetryTransactions - -The `RetryTransactions` mutation provides a mechanism to retry transactions that have previously failed. This is particularly useful for addressing transaction failures caused by temporary issues such as network congestion or gas price spikes. - - - -```graphql -mutation RetryTransactions{ - RetryTransactions(ids: [175]) -} -``` - - -```json -{ - "data": { - "RetryTransactions": true - } -} -``` - - \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/02-mutations/02-collections-mutations.md b/versioned_docs/version-2.x/03-api-reference/02-mutations/02-collections-mutations.md deleted file mode 100644 index bb51eae..0000000 --- a/versioned_docs/version-2.x/03-api-reference/02-mutations/02-collections-mutations.md +++ /dev/null @@ -1,495 +0,0 @@ ---- -title: "Collections" -slug: "collections" -description: "Perform collections mutations in the Enjin API to manage your blockchain collections, including creating, modifying, and updating assets." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - -:::tip Core Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.enjin.io/graphql` -::: - -This is a detailed reference guide that explains the most commonly used operations. - -## CreateCollection - -The `CreateCollection` mutation is used to create a new on-chain collection. A collection serves as a grouping of on-chain assets, typically non-fungible tokens (NFTs), that share common properties or belong to the same set. - - - -```graphql -mutation CreateCollection { - CreateCollection{ - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} -``` - - -```json -{ - "data": { - "CreateCollection": { - "id": 14058, - "transactionId": null, - "transactionHash": null, - "method": "CreateCollection", - "state": "PENDING", - "encodedData": "0x2800000001000000", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "ef403ad0-7b6c-4020-8ef1-01b1dc6f4513" - } - } -} -``` - - - -## ApproveCollection - -The `ApproveCollection` mutation is used to authorize another account (referred to as the "operator") to transfer tokens from a specific collection account. This operation is common in scenarios involving Non-Fungible Tokens (NFTs) or tokenized assets, where token management may need to be delegated without transferring ownership of the tokens. - - - -```graphql -mutation ApproveCollection { - ApproveCollection( - collectionId: "7154" - operator: "0x965bcdbb46614cbd79869e2eb568825f6c038cbdf9085edb1b164607d3738fa6" - expiration: 445100 - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "ApproveCollection": { - "id": 13819, - "transactionId": null, - "transactionHash": null, - "method": "ApproveCollection", - "state": "PENDING", - "encodedData": "0x280fc96f965bcdbb46614cbd79869e2eb568825f6c038cbdf9085edb1b164607d3738fa601acca0600", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## UnapproveCollection - -The `UnapproveCollection` mutation is used to revoke previously granted permissions for a specific account (referred to as the "operator") to transfer items from a collection owned by the sender's account. This operation is essential for enhancing the security and control of digital assets, ensuring that the owner of a collection can manage who has the authority to move or transfer items from their collection. - - - -```graphql -mutation UnapproveCollection{ - UnapproveCollection( - collectionId: "6305" - operator: "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "UnapproveCollection": { - "id": 14078, - "transactionId": null, - "transactionHash": null, - "method": "UnapproveCollection", - "state": "PENDING", - "encodedData": "0x28108562d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## MutateCollection - -The `MutateCollection` mutation is utilized to modify the default settings of an existing collection. This mutation allows you to make changes to various parameters of a collection, such as adjusting royalty settings. It is particularly valuable when you need to alter how royalties are distributed for the assets within a specific collection. - - - -```graphql -mutation MutateCollection { - MutateCollection( - collectionId: "10943" - mutation: { - royalty: { - beneficiary:"0x50a1ba0a184c9aca3a2ac7d427e96a676fe988454b4b56a62dd6622e843e890d" - percentage: 50 - } - } - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "MutateCollection": { - "id": 14066, - "transactionId": null, - "transactionHash": null, - "method": "MutateCollection", - "state": "PENDING", - "encodedData": "0x2802fdaa00010150a1ba0a184c9aca3a2ac7d427e96a676fe988454b4b56a62dd6622e843e890d0294357700", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## Burn - -The `Burn` mutation is used to permanently delete a specified amount of tokens from a collection. This operation is irreversible and results in the removal of tokens from circulation. When tokens are burned, the reserved value associated with them is often returned to the issuer's account. - - - -```graphql -mutation Burn { - Burn( - collectionId: "7154" - params: { - tokenId: {integer:6533} - amount:1 - keepAlive:false - } - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "Burn": { - "id": 14055, - "transactionId": null, - "transactionHash": null, - "method": "Burn", - "state": "PENDING", - "encodedData": "0x2805c96f1566040000", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## DestroyCollection - -The `DestroyCollection` mutation is used to permanently delete an existing collection. This operation is irreversible and removes the entire collection, including all associated tokens and their reserved values. - - - -```graphql -mutation DestroyCollection{ - DestroyCollection(collectionId: 10942) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "DestroyCollection": { - "id": 14062, - "transactionId": null, - "transactionHash": null, - "method": "DestroyCollection", - "state": "PENDING", - "encodedData": "0x2801f9aa", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## Freeze - -The `Freeze` mutation is used to temporarily halt token transfers on a collection, token, collection account, or token account. Freezing temporarily prevents token transfers from taking place, which also results in marketplace listings being unpurchaseable. - - - -```graphql -mutation Freeze { - Freeze( - freezeType: COLLECTION - collectionId: "10943" - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "Freeze": { - "id": 14064, - "transactionId": null, - "transactionHash": null, - "method": "Freeze", - "state": "PENDING", - "encodedData": "0x2807fdaa00", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## SetCollectionAttribute - -The `SetCollectionAttribute` mutation allows you to assign a new attribute or update an existing attribute's value within a collection. - - - -```graphql -mutation SetCollectionAttribute { - SetCollectionAttribute( - collectionId: "4741" - key: "test" - value: "Hello" - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "SetCollectionAttribute": { - "id": 14072, - "transactionId": null, - "transactionHash": null, - "method": "SetCollectionAttribute", - "state": "PENDING", - "encodedData": "0x2809154a0010746573741448656c6c6f", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## RemoveCollectionAttribute - -The `RemoveCollectionAttribute` mutation is designed to remove a specific attribute from an entire collection of tokens. Attributes typically represent metadata associated with the tokens, such as name, color, size, or any other descriptive information. - - - -```graphql -mutation RemoveCollectionAttribute { - RemoveCollectionAttribute( - collectionId: "4741" - key: "name" - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "RemoveCollectionAttribute": { - "id": 14070, - "transactionId": null, - "transactionHash": null, - "method": "RemoveCollectionAttribute", - "state": "PENDING", - "encodedData": "0x280a154a00106e616d65", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/02-mutations/03-tokens-mutations.md b/versioned_docs/version-2.x/03-api-reference/02-mutations/03-tokens-mutations.md deleted file mode 100644 index f45639e..0000000 --- a/versioned_docs/version-2.x/03-api-reference/02-mutations/03-tokens-mutations.md +++ /dev/null @@ -1,1033 +0,0 @@ ---- -title: "Tokens" -slug: "tokens" -description: "Use the Enjin API to perform tokens mutations, including minting new tokens, burning existing ones, and modifying token metadata." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - -:::tip Core Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.enjin.io/graphql` -::: - -This is a detailed reference guide that explains the most commonly used operations. - -## CreateToken - -The `CreateToken` mutation enables you to create a new token within an existing collection. This operation is essential for introducing new digital assets, and it allows you to define various attributes and characteristics for the newly created token. - - - -```graphql -mutation CreateToken { - CreateToken( - recipient: "0xc4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3" - collectionId: "10941" - params: { - tokenId:{integer:1} - initialSupply:10 - cap: { - type:INFINITE - } - metadata: {name: "Gold Coins", symbol: "GOLD", decimalCount: 2} - } - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "CreateToken": { - "id": 14060, - "transactionId": null, - "transactionHash": null, - "method": "CreateToken", - "state": "PENDING", - "encodedData": "0x280400c4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3f5aa00042800010000c16ff28623000000000000000000000000000000", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -:::info -For Token ID management, head to [Advamced Mechanics → TokenID Structure](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md) -::: - -## MintToken - -The `MintToken` mutation allows you to increase the supply of an existing token. This operation is essential for managing the circulating supply of tokens and is typically used when you need to create additional units of a specific token. - - - -```graphql -mutation MintToken { - MintToken( - recipient: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" - collectionId: "7154" - params: { - amount:1 - tokenId: {integer: 6533} - } - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "MintToken": { - "id": 14065, - "transactionId": null, - "transactionHash": null, - "method": "MintToken", - "state": "PENDING", - "encodedData": "0x280400aa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921c96f01156604010000c16ff28623000000000000000000", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## BatchMint - -The `BatchMint` mutation enables you to efficiently create multiple tokens within a single blockchain transaction. This process, known as batch minting, simplifies the minting of multiple tokens, reducing transaction fees and processing time. The maximum number of recipients that you can batch into a single transaction is 250. - - - -```graphql -mutation BatchMint { - BatchMint( - collectionId: "7154" - recipients: [ - { - account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c2921" - mintParams: { - amount:1 - tokenId: {integer: 6533} - } - }, - { - account: "0xaa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7215a17f847" - mintParams: { - amount:5 - tokenId: {integer: 4422} - } - } - ] - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "BatchMint": { - "id": 13928, - "transactionId": null, - "transactionHash": null, - "method": "BatchMint", - "state": "PENDING", - "encodedData": "0x280dc96f04aa89f9099742a928051c41eadba188ad4e863539ff96f16722ae7850271c292101156604010000c16ff28623000000000000000000", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## SetTokenAttribute - -The `SetTokenAttribute` mutation allows you to assign or update attributes for a specific token within a collection. It is used to manage individual token metadata and properties. - - - -```graphql -mutation SetTokenAttribute{ - SetTokenAttribute( - collectionId: "4741" - tokenId: {integer:0} - key: "test" - value: "Hello" - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "SetTokenAttribute": { - "id": 14073, - "transactionId": null, - "transactionHash": null, - "method": "SetTokenAttribute", - "state": "PENDING", - "encodedData": "0x2809154a010000000000000000000000000000000010746573741448656c6c6f", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## BatchSetAttribute - -The `BatchSetAttribute` mutation allows you to efficiently set or update multiple attributes for a specific token within a collection in a single blockchain transaction. Attributes represent various properties, characteristics, or metadata associated with a token. - - - -```graphql -mutation BatchSetAttribute { - BatchSetAttribute( - collectionId: "7154" - tokenId: {integer:6533} - attributes: [ - { - key:"Name" - value:"John" - }, - { - key:"Surname" - value:"Snow" - }, - { - key:"House" - value:"Stark" - } - ] - ) { - id - transactionId - transactionHash - method - encodedData - state - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "BatchSetAttribute": { - "id": 13961, - "transactionId": null, - "transactionHash": null, - "method": "BatchSetAttribute", - "encodedData": "0x280ec96f01851900000000000000000000000000000410746573741468656c6c6f", - "state": "PENDING", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## BatchTransfer - -The `BatchTransfer` mutation streamlines the process of transferring multiple tokens to one or more recipients within a single blockchain transaction. This is particularly useful when you need to distribute tokens to various accounts, such as rewarding multiple users or dispersing assets to different stakeholders. - - - -```graphql -mutation BatchTransfer { - BatchTransfer( - collectionId: 36105 - recipients: [ - { - account:"cxM1YqYERiNvuEKkZUiUFefCtTfdy2q4Bj7QuWDfSPJuiuDWy" - simpleParams: { - tokenId: {integer:1} - amount:1 - } - }, - { - account:"cxKLTnnbsjsWQen47Sb73WfvCSsWAQfjNd6hRW86EqKC9Frhu" - simpleParams: { - tokenId: {integer:1} - amount:2 - } - }, - { - account:"cxM1YqYERiNvuEKkZUiUFefCtTfdy2q4Bj7QuWDfSPJuiuDWy" - simpleParams: { - tokenId: {integer:4} - amount:1 - } - } - ] - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "BatchTransfer": { - "id": 13974, - "transactionId": null, - "transactionHash": null, - "method": "BatchTransfer", - "state": "PENDING", - "encodedData": "0x280cc96f041013860712045df3393a7902eba3621115fdb7a6a4837349f7bc2fb676d57c000015660400", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## SimpleTransferToken - -The `SimpleTransferToken` mutation simplifies the process of transferring a specific token from one wallet to another. It is a straightforward way to facilitate token transfers without the need for complex intermediary steps. - - - -```graphql -mutation SimpleTransferToken { - SimpleTransferToken( - collectionId: "7154" - recipient: "0xc4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3" - params: { - tokenId: {integer:70468841277235617716769448539773927607} - amount:1 - } - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "SimpleTransferToken": { - "id": 14074, - "transactionId": null, - "transactionHash": null, - "method": "SimpleTransferToken", - "state": "PENDING", - "encodedData": "0x280600c4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3c96f0033b7007b94fad4fa2fe1d6a5b71ece03350400", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## ApproveToken - -The `ApproveToken` mutation is a powerful tool for establishing fine-grained permission systems at the individual token level within a specific token collection. It enables a token owner to grant another account (the operator) the authority to transfer a defined number of tokens from their ownership to other accounts. This level of control is particularly valuable when managing tokens with varying values, attributes, or use cases within a collection. - - - -```graphql -mutation ApproveToken { - ApproveToken( - collectionId: "7154" - tokenId: {integer:6533} - operator: "0x965bcdbb46614cbd79869e2eb568825f6c038cbdf9085edb1b164607d3738fa6" - amount: 1 - currentAmount: 1 - expiration: 445300 - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "ApproveToken": { - "id": 13855, - "transactionId": null, - "transactionHash": null, - "method": "ApproveToken", - "state": "PENDING", - "encodedData": "0x2811c96f1566965bcdbb46614cbd79869e2eb568825f6c038cbdf9085edb1b164607d3738fa6040174cb060004", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## UnapproveToken - -The `UnapproveToken` mutation serves the purpose of revoking approval for a specific account (referred to as the "operator") to transfer a particular token from a token account. Unlike `UnapproveCollection`, which revokes permissions over an entire collection, `UnapproveToken` provides fine-grained control by targeting individual tokens within a collection. This level of precision is valuable when managing tokens with distinct values, attributes, or permissions within a collection. - - - -```graphql -mutation UnapproveToken{ - UnapproveToken( - collectionId: 7154 - tokenId: {integer:70468841277235617716769448539773927607} - operator: "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "UnapproveToken": { - "id": 14079, - "transactionId": null, - "transactionHash": null, - "method": "UnapproveToken", - "state": "PENDING", - "encodedData": "0x2812c96f33b7007b94fad4fa2fe1d6a5b71ece0335d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## Freeze - -The `Freeze` mutation is a powerful tool that enables the temporary suspension of token transfers on a collection, token, collection account, or token account. This freezing action effectively prevents any further transfers from occurring, which also results in marketplace listings being unpurchaseable. The primary purpose of freezing is to provide administrative control, enhance security, investigate suspicious activities, or respond to unforeseen events that require the temporary halt of operations. - - - -```graphql -mutation Freeze { - Freeze( - freezeType: COLLECTION - collectionId: "10943" - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "Freeze": { - "id": 14064, - "transactionId": null, - "transactionHash": null, - "method": "Freeze", - "state": "PENDING", - "encodedData": "0x2807fdaa00", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## Thaw - -The `Thaw` mutation is a vital operation that allows you to unfreeze a previously frozen collection or token. Freezing temporarily prevents any transfers from occurring. Thawing reverses this action, restoring the collection or token's ability to be traded and transferred. - - - -```graphql -mutation Thaw{ - Thaw( - freezeType: TOKEN - collectionId: 7154 - tokenId: {integer:70468841277235617716769448539773927607} - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "Thaw": { - "id": 14075, - "transactionId": null, - "transactionHash": null, - "method": "Thaw", - "state": "PENDING", - "encodedData": "0x2808c96f01b7007b94fad4fa2fe1d6a5b71ece03350101", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## MutateToken - -The `MutateToken` mutation is a powerful tool for modifying specific properties of an individual token within a collection. It provides fine-grained control over token attributes, allowing you to make precise changes to a token's characteristics. In the provided example, the mutation is used to set the `listingForbidden` attribute to `true`, which effectively prohibits the token from being listed for sale or trade. - - - -```graphql -mutation MutateToken { - MutateToken( - collectionId: "7154" - tokenId: {integer:6533} - mutation: { - listingForbidden:true - } - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "MutateToken": { - "id": 14067, - "transactionId": null, - "transactionHash": null, - "method": "MutateToken", - "state": "PENDING", - "encodedData": "0x2803c96f15660100010100", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## OperatorTransferToken - -The `OperatorTransferToken` mutation is designed for operators to transfer tokens on behalf of the token owner. It enables the transfer of a specified amount of tokens from a source address to a recipient address, with the operation performed by an authorized operator. - - - -```graphql -mutation OperatorTransferToken{ - OperatorTransferToken( - collectionId: "7154" - recipient: "0x50a1ba0a184c9aca3a2ac7d427e96a676fe988454b4b56a62dd6622e843e890d" - params: { - tokenId: {integer:6533} - amount:1 - source:"0x1013860712045df3393a7902eba3621115fdb7a6a4837349f7bc2fb676d57c00" - } - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "OperatorTransferToken": { - "id": 14068, - "transactionId": null, - "transactionHash": null, - "method": "OperatorTransferToken", - "state": "PENDING", - "encodedData": "0x28060050a1ba0a184c9aca3a2ac7d427e96a676fe988454b4b56a62dd6622e843e890dc96f0115661013860712045df3393a7902eba3621115fdb7a6a4837349f7bc2fb676d57c000400", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## RemoveAllAttributes - -Remove all attributes from a collection and token. - - - -```graphql -mutation RemoveAllAttributes { - RemoveAllAttributes( - collectionId: "7154" - tokenId: {integer:6533} - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "RemoveAllAttributes": { - "id": 14069, - "transactionId": null, - "transactionHash": null, - "method": "RemoveAllAttributes", - "state": "PENDING", - "encodedData": "0x280bc96f018519000000000000000000000000000001000000", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## RemoveTokenAttribute - -The `RemoveTokenAttribute` mutation provides the capability to remove a specific attribute from an individual token within a collection. This operation is distinct from the `RemoveCollectionAttribute` mutation, which removes an attribute from all tokens within a collection. It is a precise way to manage token metadata, allowing for the removal of attributes that are no longer needed or contain errors. - - - -```graphql -mutation RemoveTokenAttribute { - RemoveTokenAttribute( - collectionId: "4741" - tokenId: {integer:0} - key: "name" - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "RemoveTokenAttribute": { - "id": 14071, - "transactionId": null, - "transactionHash": null, - "method": "RemoveTokenAttribute", - "state": "PENDING", - "encodedData": "0x280a154a0100000000000000000000000000000000106e616d65", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## TransferBalance - -The `TransferBalance` mutation is an operation designed for initiating the transfer of a specified amount of funds or balance from one account to another. It plays a fundamental role in enabling financial transactions by allowing users to send precise amounts of cryptocurrency or digital assets securely and transparently. - - - -```graphql -mutation TransferBalance { - TransferBalance( - recipient: "0xc4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3" - amount: 1 - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "TransferBalance": { - "id": 14077, - "transactionId": null, - "transactionHash": null, - "method": "TransferBalance", - "state": "PENDING", - "encodedData": "0x0a0300c4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a304", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## TransferAllBalance - -The `TransferAllBalance` mutation is designed for the purpose of transferring the entire balance from one account to another. It simplifies the process of moving all available funds from a sender's account to a recipient's account. This mutation is particularly useful for scenarios where you need to empty an account, consolidate funds from multiple accounts into one, or transfer ownership of an account by moving all assets to the new owner. - - - -```graphql -mutation TransferAllBalance { - TransferAllBalance( - recipient: "0xc4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a3" - ) { - id - transactionId - transactionHash - method - state - encodedData - wallet { - account { - publicKey - address - } - } - } -} -``` - - -```json -{ - "data": { - "TransferAllBalance": { - "id": 14076, - "transactionId": null, - "transactionHash": null, - "method": "TransferAllBalance", - "state": "PENDING", - "encodedData": "0x0a0400c4571a3720604eb1901dad5444376f91409814a2806686c6e6c4f2d664b422a301", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - } - } - } -} -``` - - - -## Infuse - -The Infuse mutation allows you to embed Enjin Coin (ENJ) into an existing token. This operation is crucial for enhancing the intrinsic value of digital assets, ensuring that they carry a guaranteed ENJ backing. By infusing ENJ, you provide tangible value to the token, which can only be retrieved through the process of "ing" the token. - -:::warning Calculating the infusion amount -The Platform accepts infusion values in the **base unit** (integers), not decimal ENJ amounts. To calculate the correct input, **multiply your desired ENJ amount by 10^18** (1 quintillion). - - **Formula:** `Desired ENJ infusion` \* `1,000,000,000,000,000,000` - - **Example:** To infuse a token with **5 ENJ**, input `5000000000000000000`. -::: - - - -```graphql -mutation Infuse{ - Infuse( - collectionId: 3298 #Specify the collection ID - tokenId: {integer: 1} #Specify the token ID - amount: 5000000000000000000 #Specify the amount of ENJ to infuse - ){ - id - method - state - } -} -``` - - -```json -{ - "data": { - "Infuse": { - "id": 386279, - "method": "Infuse", - "state": "PENDING" - } - } -} -``` - - diff --git a/versioned_docs/version-2.x/03-api-reference/02-mutations/04-wallets-mutations.md b/versioned_docs/version-2.x/03-api-reference/02-mutations/04-wallets-mutations.md deleted file mode 100644 index 7af0d0f..0000000 --- a/versioned_docs/version-2.x/03-api-reference/02-mutations/04-wallets-mutations.md +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: "Wallets" -slug: "wallets" -description: "Perform wallets mutations in the Enjin API to manage wallet data, transfer assets, and update balances securely on the blockchain." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - -:::tip Core Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql` -- **Mainnet:** `http://platform.enjin.io/graphql` -::: - -This is a detailed reference guide that explains the most commonly used operations. - -## CreateWallet - -The `CreateWallet` mutation in is used to create a new wallet on the blockchain using an external identifier (external ID). This allows you to associate a wallet with an entity on the Enjin Platform. - - - -```graphql -mutation CreateWallet { - CreateWallet( - externalId: "e73f9f38-6832-4822-922b-b9225245ba24" - ) -} -``` - - -```json -{ - "data": { - "CreateWallet": true - } -} -``` - - - -## SetWalletAccount - -The `SetWalletAccount` mutation is used to associate an existing wallet account on the Enjin Platform with a game developer's external ID. This action links the user's wallet on the Enjin Platform to an external ID stored in the game developer's database, allowing the identification of a player's wallet within the Enjin Platform. - - - -```graphql -mutation SetWalletAccount{ - SetWalletAccount( - externalId: "ZMMP0FCq6NEKphFlNfESdbC81BXWebNHUiguJvFsb5dVnthIvBbFAaNfCE3mOJREDPS5lwWN4sDQXDV61fqRWUKVSeq4ItavScDS" - account: "cxLc8HSeuiLiYmEG7XB8wQrbkRPNCaDi5dVpqPkyhWdEp6i63" - ) -} -``` - - -```json -{ - "data": { - "SetWalletAccount": true - } -} -``` - - - -## VerifyAccount - -The `VerifyAccount` mutation is used by a wallet to confirm the ownership of a user account. This security measure ensures that actions performed on the account are authorized by the legitimate owner, enhancing the overall security and trustworthiness of the system. - - - -```graphql -mutation VerifyAccount{ - VerifyAccount( - verificationId: "0xfdb976d1c5fc9ed6d546f31332e756ca9c9ef6e3c6f0d9ea0ed0713b1cd84440" - signature: "0x8958f002f9513256fa329fa92441d39adc59e3a19165aeeb343acd72b7f6a" - account: "0xbeac4e574b46a3722da9239f97359b0440c0fb66e3b130f20c2284fd033f9138" - - ) -} -``` - - -```json -{ - "data": { - "VerifyAccount": true - } -} -``` - - - -## UpdateWalletExternalId - -The `UpdateWalletExternalId` mutation is used to update the external ID associated with a wallet account on the Enjin Platform. This action allows you to change the reference or identifier linked to a user's wallet within the Enjin Platform. - - - -```graphql -mutation UpdateWalletExternalId{ - UpdateWalletExternalId( - id: 8466 - newExternalId: "35b409dc-4f3f-41da-8d94-7635ef2a5853" - ) -} -``` - - -```json -{ - "data": { - "UpdateWalletExternalId": true - } -} -``` - - \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/02-mutations/05-fuel-tanks-mutations.md b/versioned_docs/version-2.x/03-api-reference/02-mutations/05-fuel-tanks-mutations.md deleted file mode 100644 index a5d9ef3..0000000 --- a/versioned_docs/version-2.x/03-api-reference/02-mutations/05-fuel-tanks-mutations.md +++ /dev/null @@ -1,827 +0,0 @@ ---- -title: "Fuel Tanks" -slug: "fuel-tanks" -description: "Use the Enjin API to perform fuel tanks mutations, including setting up and managing transaction cost coverage for users on the blockchain." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - -:::tip Fuel Tanks Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql/fuel-tanks` -- **Mainnet:** `http://platform.enjin.io/graphql/fuel-tanks` -::: - -This is a detailed reference guide that explains the most commonly used operations. - -## CreateFuelTank - -The `CreateFuelTank` mutation is used to create a new fuel tank within a system, allowing you to establish specific attributes and rules that govern how accounts can be added and how transactions are dispatched from it. This mutation is a fundamental step in setting up the infrastructure for managing accounts and transactions within a network, providing a customizable framework to tailor the tank's behavior to your specific requirements. - - - -```graphql -mutation CreateFuelTank { - CreateFuelTank( - name: "My Fuel Tank" - coveragePolicy: FEES_AND_DEPOSIT - reservesAccountCreationDeposit: false - requireAccount: true - accountRules: {requireToken: {collectionId: "7406", tokenId: {integer: 5839}}, whitelistedCallers: ["0x66f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f646009"]} - dispatchRules: [{requireToken: {collectionId: "7406", tokenId: {integer: 5839}}, whitelistedCallers: ["0x66f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f646009"], whitelistedCollections: null, maxFuelBurnPerTransaction: 0, userFuelBudget: {amount: 1, resetPeriod: 1}, tankFuelBudget: {amount: 1, resetPeriod: 1}}] - ) { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} -``` - - -```json -{ - "data": { - "CreateFuelTank": { - "id": 379503, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x3600404d79204675656c2054616e6b203132330100040000000014000466f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f64600905ee1c0000000000000000000000000000cf1600000000000000000000000000000200000000000000000000000000000000030401000000040401000000010108000466f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f64600901ee1c0000000000000000000000000000cf160000000000000000000000000000", - "method": "CreateFuelTank", - "wallet": null, - "idempotencyKey": "ba66208f-d565-4605-bc6b-58756f88814e" - } - } -} -``` - - - -## DestroyFuelTank - -The `DestroyFuelTank` mutation is used to permanently remove an existing fuel tank from the system. This operation is essential for efficient resource management, allowing you to clean up unused or unnecessary fuel tanks within your network infrastructure. - - - -```graphql -mutation DestroyFuelTank { - DestroyFuelTank(tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr") { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} -``` - - -```json -{ - "data": { - "DestroyFuelTank": { - "id": 11280, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x360d009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d", - "method": "DestroyFuelTank", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "4c0440c5-340f-4de6-a3df-49f464f46656" - } - } -} -``` - - - -## AddAccount - -The `AddAccount` mutation is used to add a new account to an existing fuel tank. This operation is crucial when you need to grant access to specific users, allowing them to utilize the resources and perform authorized actions within the fuel tank. - - - -```graphql -mutation AddAccount { - AddAccount( - tankId: "cxNkbpQpiiza3JCH9fxmt6eJ8vDSFqzqVfz2a543QbPG4sN9K" - userId: "cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts" - ) { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} -``` - - -```json -{ - "data": { - "AddAccount": { - "id": 11249, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x360200bf71212e94365d5d3c5e7b2700a3bb9e2a19016b6502f625162024eb87fdcf7c00985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050", - "method": "AddAccount", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "da44d553-6a39-460b-9dd8-b35c6984016c" - } - } -} -``` - - - -## BatchAddAccount - -The `BatchAddAccount` mutation is designed for efficiently adding multiple new user accounts to a specified fuel tank in a single batch operation. This feature is particularly useful when you need to add several accounts at once, reducing the number of individual requests and optimizing resource usage. - - - -```graphql -mutation BatchAddAccount { - BatchAddAccount(tankId: "cxNkbpQpiiza3JCH9fxmt6eJ8vDSFqzqVfz2a543QbPG4sN9K", userIds: ["cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts","cxLc8HSeuiLiYmEG7XB8wQrbkRPNCaDi5dVpqPkyhWdEp6i63"]) { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} - -``` - - -```json -{ - "data": { - "BatchAddAccount": { - "id": 11256, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x360a00bf71212e94365d5d3c5e7b2700a3bb9e2a19016b6502f625162024eb87fdcf7c0800985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d0500060820310ea5b09bef944c50a7f3ae82166304dd637263b1ba7e0fa0bab1f3f7b", - "method": "BatchAddAccount", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "76f65815-8743-4ab9-8e01-b3fc62137337" - } - } -} -``` - - - -## BatchRemoveAccount - -The `BatchRemoveAccount` mutation is designed for efficiently removing multiple user accounts from a specified fuel tank in a single batch operation. This feature is particularly useful when you need to remove several accounts at once, reducing the number of individual removal requests and optimizing resource usage. - - - -```graphql -mutation BatchRemoveAccount { - BatchRemoveAccount(tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr", userIds: ["cxLkamf2QxHK9bcqhh56QAFc8TTmvWG33sUbkNakJV4yzdDcz"]) { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} -``` - - -```json -{ - "data": { - "BatchRemoveAccount": { - "id": 11279, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x360b009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d040066f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f646009", - "method": "BatchRemoveAccount", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "af81b86e-2a71-4cf7-8784-60b777fed366" - } - } -} -``` - - - -## Dispatch - -The `Dispatch` mutation is a powerful operation that allows you to trigger specific actions within a system using a designated fuel tank (`tankId`) while adhering to predefined rules specified by a rule set (`ruleSetId`). This mutation is especially useful when you want the fuel tank to handle transaction fees and potentially storage deposits for the dispatched call. - - - -```graphql -mutation Dispatch { - Dispatch( - tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr" - ruleSetId: 0 - dispatch: { - call:FUEL_TANKS - query:"mutation { AddAccount(tankId: \"cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr\", userId: \"cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts\") { id encodedData } }" - variables:null - } - paysRemainingFee: false - ) { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} -``` - - -```json -{ - "data": { - "Dispatch": { - "id": 11286, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x3605009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d000000003602009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d00985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d05000", - "method": "Dispatch", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "850956b8-b9e9-4e2d-98f4-ae7775e2d066" - } - } -} -``` - - - -## DispatchAndTouch - -The `DispatchAndTouch` mutation combines the functionalities of the `Dispatch` mutation with the ability to create an account for the calling entity (`origin`) if it doesn't already exist. This mutation is especially valuable in scenarios where the caller's account may not have been set up in advance, streamlining the process of both executing an operation and ensuring account existence. - - - -```graphql -mutation DispatchAndTouch { - DispatchAndTouch( - tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr" - ruleSetId: 0 - dispatch: { - call:FUEL_TANKS - query:"mutation { AddAccount(tankId: \"cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr\", userId: \"cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts\") { id encodedData } }" - variables:null - } - paysRemainingFee: false - ) { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} -``` - - -```json -{ - "data": { - "DispatchAndTouch": { - "id": 11284, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x3606009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d000000003602009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d00985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d05000", - "method": "DispatchAndTouch", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "a9e62906-ac2f-4454-b17e-c0e5bf774da0" - } - } -} -``` - - - -## SetConsumption - -The `SetConsumption` mutation enables you to manually set the fuel consumption for a specific fuel tank. It allows you to override the current fuel consumption data with the values provided in the mutation arguments. This can be useful for making adjustments, corrections, or administrative changes to fuel consumption records. - - - -```graphql -mutation SetConsumption { - SetConsumption( - tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr" - ruleSetId: 0 - userId: "cxLkamf2QxHK9bcqhh56QAFc8TTmvWG33sUbkNakJV4yzdDcz" - totalConsumed: 10 - lastResetBlock: 100 - ) { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} -``` - - -```json -{ - "data": { - "SetConsumption": { - "id": 11287, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x360c009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d010066f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f64600900000000280164000000", - "method": "SetConsumption", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "9bc5fc3d-d5d2-415c-a0bf-96e5de529502" - } - } -} -``` - - - -## InsertRuleSet - -The `InsertRuleSet` mutation is used to add a new rule set to a fuel tank. It allows you to define specific rules and requirements for the fuel tank's operation. - - - -```graphql -mutation InsertRuleSet { - InsertRuleSet( - tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr" - ruleSetId: 1 - dispatchRules: { requireToken: {collectionId: "7153", tokenId: {integer: 68}} } - ) { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} -``` - - -```json -{ - "data": { - "InsertRuleSet": { - "id": 11288, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x3608009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d0100000000", - "method": "InsertRuleSet", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "2ed39eca-dc04-408d-ac99-1b44c19e2e90" - } - } -} -``` - - - -### Rules - -When a call is made to a fuel tank, it must be made in accordance with a set of rules. These rules, known as rule sets, can include multiple individual rules that determine the validity of the call. A fuel tank can have multiple rule sets, each of which controls access and permissions to the fuel tank's functionality and resources. - -##### Whitelisted Callers - -Whitelisted callers are accounts that are explicitly granted permission to make calls to a fuel tank. Any calls made by accounts not on the whitelist will be rejected and fail. - -##### Whitelisted Collections - -Whitelisted collections refer to a list of specific collections allowed to be called by dispatch on a fuel tank. If a dispatch call is made to any collection, not on the whitelist, it will be rejected and fail. This ensures that calls are only made to the specific collections authorized to be accessed. - -##### Max Fuel Burn Per Transaction - -It is a setting that limits the amount of fuel that can be consumed in a single transaction. It is a safety measure that helps prevent misuse and overuse of the fuel tank's resources by ensuring that a single transaction doesn't consume too much fuel, which is important to ensure a sustainable network. - -##### User Fuel Budget - -A user fuel budget is the total amount of fuel allocated to a specific user across all rule sets of a fuel tank. It sets a limit on the amount of fuel that can be consumed by a user's transactions. Once the user's fuel budget is exhausted, their transactions will fail until more fuel is added to their budget or their overall consumption is lowered , which is meant to avoid the overuse of resources and ensure a sustainable network.\*\* - -##### Tank Fuel Budget - -A tank fuel budget is the total amount of fuel allocated to a specific fuel tank across all its rule sets. It sets a limit on the amount of fuel that can be consumed by all transactions made through that tank. Once the tank's fuel budget is exhausted, all transactions will fail until more fuel is added to the tank's budget or overall consumption is lowered. - -##### Require Token - -The "Require token" setting indicates that a specific token must be held by the caller for their call to be accepted by the fuel tank. If the caller does not possess the required token, their call will be rejected and fail. This feature is intended to ensure that only users who hold the specified token can access the fuel tank's functions and resources. - -##### Permitted Calls - -Permitted calls refer to a list of specific function calls that are allowed to be made using this rule set. Any calls made to other functions will be rejected and fail. This ensures that only authorized calls can be made by the users, and also, by setting a boundary to what calls are allowed and what calls are not, it helps to protect the network from unwanted transactions that could impact negatively on the network. - -##### Permitted Extrinsics - -Permitted extrinsics refer to a list of specific extrinsics that are allowed to be made using this rule set. Any extrinsics that are not on the list will be rejected and fail. This feature is used to restrict access to specific functionality and resources on the network and ensure that only authorized calls can be made by users. When setting this rule, it's important to note that the parameters passed to the extrinsic do not affect the outcome, only the function name is considered.\*\* - -##### Freezing - -"Freezing" is a state where a fuel tank or a rule set is temporarily prevented from accepting calls or making changes. This means that while a fuel tank or rule set is frozen, no dispatches are allowed to occur on it. This is implemented as a safety measure to prevent accidental or malicious changes and to ensure that the fuel tank or rule set remains in a stable state until the freeze is lifted. - -##### Descriptor - -A descriptor is a list of all the data needed to create a fuel tank. It includes details such as the fuel tank's name, account management rules, rule sets and other configuration information related to the fuel tank. The descriptor acts as a blueprint that defines how the fuel tank is set up and how it will function. It contains all the information needed to create and configure the fuel tank, and can be used to modify the fuel tank's settings. - -## MutateFuelTank - -The `MutateFuelTank` mutation is used to apply a specific mutation to a fuel tank. This mutation allows you to modify the Fuel Tank configuration. - - - -```graphql -mutation MutateFuelTank { - MutateFuelTank(tankId: "cxLt3kZZHgz4pqGgaQWjxCRXGd81ey7YRwycGcRnZrNtfpbAs", - mutation: { - coveragePolicy: FEES_AND_DEPOSIT, - reservesAccountCreationDeposit: true - accountRules: { - whitelistedCallers: [ - "cxKy7aqhQTtoJYUjpebxFK2ooKhcvQ2FQj3FePrXhDhd9nLfu", - "cxKRcxyqEuj8qwS4qAmxZMLKNoMJPMhQBLhoQdKekubbo3BtP" - ] - } - } - ){ - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} -``` - - -```json -{ - "data": { - "MutateFuelTank": { - "id": 386329, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x3601006ca6ae51167284d08d113d0d04dd502e7c827fff54d07c9215f601c202f335560101010101010400082c42a1565cf51b8c6ed5ac06ebb7c1c5999ff0076879fd1ba746c9b5d5029d3644479350952f776be51e7b79010a71203cb18d805fbaaf64990ae40790007525", - "method": "MutateFuelTank", - "wallet": null, - "idempotencyKey": "9ffdd07e-a04a-4116-affa-b73ad3c8f4d8" - } - } -} -``` - - - -## RemoveAccount - -The `RemoveAccount` mutation is used to remove a specified user account from a fuel tank. This mutation is particularly useful for managing the accounts associated with a fuel tank. - - - -```graphql -mutation RemoveAccount { - RemoveAccount(tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr", userId: "cxLkamf2QxHK9bcqhh56QAFc8TTmvWG33sUbkNakJV4yzdDcz") { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} -``` - - -```json -{ - "data": { - "RemoveAccount": { - "id": 11291, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x3603009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d0066f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f646009", - "method": "RemoveAccount", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "c63c8c9d-3fcb-412a-afac-703da5c2b145" - } - } -} -``` - - - -## RemoveAccountRuleData - -The `RemoveAccountRuleData` mutation is used to delete specific rule data associated with an account on a fuel tank. This allows you to remove certain restrictions or requirements that were previously set for an account's interactions with the fuel tank. - - - -```graphql -mutation RemoveAccountRuleData { - RemoveAccountRuleData( - tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr" - ruleSetId: 0 - userId: "cxLkamf2QxHK9bcqhh56QAFc8TTmvWG33sUbkNakJV4yzdDcz" - rule: REQUIRE_TOKEN - ) { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} -``` - - -```json -{ - "data": { - "RemoveAccountRuleData": { - "id": 11292, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x3604009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d0066f522f1e17f25b3942916dcc60d92f0f8bc27d40083fadf5d65e9dd5f6460090000000005", - "method": "RemoveAccountRuleData", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "d3e79021-c7a4-4a28-a29d-effd90d95933" - } - } -} -``` - - - -## RemoveRuleSet - -The `RemoveRuleSet` mutation is utilized to completely remove an existing set of rules from a specified fuel tank. Rule sets represent collections of conditions or constraints that govern the usage and access to the fuel tank. Removing a rule set becomes necessary when you need to modify operational policies, or a rule set is no longer applicable to the fuel tank's operations. - - - -```graphql -mutation RemoveRuleSet { - RemoveRuleSet(tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr", ruleSetId: 0) { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} -``` - - -```json -{ - "data": { - "RemoveRuleSet": { - "id": 11293, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x3609009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d00000000", - "method": "RemoveRuleSet", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "6f05d6c2-69ee-42ca-8a5c-f55c8a34da8b" - } - } -} -``` - - - -## ScheduleMutateFreezeState - -The `ScheduleMutateFreezeState` mutation enables you to schedule a change in the `is_frozen` state of either a fuel tank or a rule set. This state determines whether the tank or rule set can be frozen (immobilized), preventing any operations that could alter its configuration or state. - - - -```graphql -mutation ScheduleMutateFreezeState { - ScheduleMutateFreezeState( - tankId: "cxMyyy9aiu8Wv8PDtnjkuSKzJ7n854hQNcNETZiHVS3s3xPhr" - ruleSetId: 0 - isFrozen: true - ) { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} -``` - - -```json -{ - "data": { - "ScheduleMutateFreezeState": { - "id": 11294, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x3607009d69a6f1d45b1dabfcc1a365292753663444bdacb429035b26dac32660dca60d010000000001", - "method": "ScheduleMutateFreezeState", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "e2609c5a-7b0d-4f1a-88c8-cf92c9491549" - } - } -} -``` - - \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/02-mutations/06-marketplace-mutations.md b/versioned_docs/version-2.x/03-api-reference/02-mutations/06-marketplace-mutations.md deleted file mode 100644 index 7105d7b..0000000 --- a/versioned_docs/version-2.x/03-api-reference/02-mutations/06-marketplace-mutations.md +++ /dev/null @@ -1,349 +0,0 @@ ---- -title: "Marketplace" -slug: "marketplace" -description: "Use the Enjin API to perform marketplace mutations, including creating and modifying token listings, managing trades, and marketplace transactions." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - -:::tip Marketplace Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql/marketplace` -- **Mainnet:** `http://platform.enjin.io/graphql/marketplace` -::: - -This is a detailed reference guide that explains the most commonly used operations. - -## CreateListing - -The `CreateListing` mutation is used to initiate the process of placing a buy or a sell order on a marketplace. This mutation is essential for sellers who want to list their assets for sale in the marketplace, or buyers who wants to purchase a token. Buy orders are Offers, and sell orders are either Fixed Listings or Auction Listings. - -### Use Case: - -The `CreateListing` mutation is a fundamental operation in a marketplace. It allows you, as a seller, to list your assets for sale on the platform, and as a buyer, to create a buy offer. Here's how you can use it: - -1. **Asset Details**: Specify the asset you want to buy/sell and the asset you want to receive in exchange. - When creating a sell order, specify the asset you want to sell in the `makeAssetId` parameter, and the asset you want to receive in exchange in the `takeAssetId` parameter. - When creating a buy order, specify the asset you want to buy in the `takeAssetId` parameter, and the asset you want to pay with in the `makeAssetId` parameter. - -:::info -For ENJ tokens, use `collectionId: 0` and `tokenId: {integer: 0}` -::: - -2. **Quantity and Price**: Set the quantity (`amount`) and the price (`price`) at which you are willing to buy/sell the asset. -3. **Uniqueness**: Include a random string (`salt`) to ensure the uniqueness of your listing order. -4. **start/end block (Optional)**: If you are conducting an auction, you can specify the start and end blocks for the auction (`auctionParams`). - If you are conducting an offer, you can specify the end block for the auction (`offerParams`). -5. **Response**: After successfully creating the listing, you will receive a response with details about the new listing, including its ID, state, and other relevant information. - -This mutation is essential for both buyers and sellers to participate in the marketplace, list their assets and offers, and potentially initiate sales or auctions. It forms the basis for trading assets within the platform. - -### Create Fixed Listing - - - -```graphql -mutation CreateOffer{ - CreateListing( - makeAssetId: { - collectionId: 91829 - tokenId: {integer: 1} - } - takeAssetId: { - collectionId: 0 - tokenId: {integer: 0} - } - amount: 1 - price: 10000000000000000000 - salt: "random string" - listingData: { - type: FIXED_PRICE - } - ){ - id - method - state - } -} -``` - - -```json -{ - "data": { - "CreateListing": { - "id": 386427, - "method": "CreateListing", - "state": "PENDING" - } - } -} -``` - - - -### Create Auction Listing - - - -```graphql -mutation CreateOffer{ - CreateListing( - makeAssetId: { - collectionId: 91829 - tokenId: {integer: 1} - } - takeAssetId: { - collectionId: 0 - tokenId: {integer: 0} - } - amount: 1 - price: 5000000000000000000 - salt: "random string" - listingData: { - type: AUCTION - auctionParams: { - startBlock: 3818000 - endBlock: 1000000000 - } - } - ){ - id - method - state - } -} -``` - - -```json -{ - "data": { - "CreateListing": { - "id": 386422, - "method": "CreateListing", - "state": "PENDING" - } - } -} -``` - - - -### Create Offer Listing - - - -```graphql -mutation CreateOffer{ - CreateListing( - makeAssetId: { - collectionId: 0 - tokenId: {integer: 0} - } - takeAssetId: { - collectionId: 91829 - tokenId: {integer: 1} - } - amount: 1 - price: 5000000000000000000 - salt: "random string" - listingData: { - type: OFFER - offerParams: { - expiration: 100000000 - } - } - ){ - id - method - state - } -} -``` - - -```json -{ - "data": { - "CreateListing": { - "id": 386394, - "method": "CreateListing", - "state": "PENDING" - } - } -} -``` - - - -## FillListing - -The `FillListing` mutation is used to finalize the purchase of an asset listed at a fixed price on a marketplace, as well as offers. It enables buyers to accept the terms of a listing and execute the transaction to acquire the asset they desire and enables sellers to accept an offer. - - - -```graphql -mutation FillListing { - FillListing(listingId: "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20", amount: 1) { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} -``` - - -```json -{ - "data": { - "FillListing": { - "id": 11297, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x3202101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f2004", - "method": "FillListing", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "0f4eb316-cf7f-4561-a2d6-112abf08d323" - } - } -} -``` - - - -### Use Case: - -The `FillListing` mutation is a critical operation for buyers in a marketplace. Here's how you can use it: - -1. **Select Listing**: You specify the unique `listingId` of the listing you want to fill, indicating the asset you wish to purchase. -2. **Quantity**: Determine the `amount` of the asset you want to buy. Ensure that this quantity matches the available quantity in the listing if it's a fixed price sale. -3. **Transaction Submission**: Execute the mutation, triggering the transaction. The response will provide transaction details, including the initial state as "PENDING." -4. **Transaction Processing**: The network processes the transaction, and eventually, the `transactionId` and `transactionHash` fields in the response will be updated with the respective values once confirmed. -5. **Completion**: The successful execution of the `FillListing` mutation indicates that you have committed to the purchase, and the asset will be transferred to your wallet upon transaction confirmation. -6. **Accepting Offers**: The mutation is also used by seller to accept a buy offer and sell the asset to the buyer. - -This mutation is essential for buyers to participate in the marketplace, finalize purchases, and take ownership of assets. It facilitates the transfer of both digital assets and funds between parties on the blockchain, ensuring a seamless and secure transaction process. - -## FinalizeAuction - -The `FinalizeAuction` mutation is used to complete the auction process for a specific asset listed in a marketplace. This operation is crucial for officially closing the auction and transferring the asset to the highest bidder, typically after the auction has ended. - - - -```graphql -mutation FinalizeAuction { - FinalizeAuction(listingId: "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20") { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} -``` - - -```json -{ - "data": { - "FinalizeAuction": { - "id": 11298, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x3204101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20", - "method": "FinalizeAuction", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "0deaaad8-f6a0-4ef5-b8f1-fadd1ecb8ac5" - } - } -} -``` - - - -## CancelListing - -The `CancelListing` mutation in used to cancel a listing on the Enjin Blockchain marketplace. This operation is typically performed by the seller or the system administrator and is used to remove an asset from a marketplace listing. - - - -```graphql -mutation CancelListing { - CancelListing(listingId: "0x101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20") { - id - transactionId - transactionHash - state - encodedData - method - wallet { - account { - publicKey - address - } - } - idempotencyKey - } -} -``` - - -```json -{ - "data": { - "CancelListing": { - "id": 11295, - "transactionId": null, - "transactionHash": null, - "state": "PENDING", - "encodedData": "0x3201101d16dc0fa25d77a92bcb6cde34e8ad1e85d96cd6a4a78eb68acc4f82d37f20", - "method": "CancelListing", - "wallet": { - "account": { - "publicKey": "0x68b427dda4f3894613e113b570d5878f3eee981196133e308c0a82584cf2e160", - "address": "cxLnsZcpE1xETr7TQrMCCsRYpSfpHPUpJUFAfiZdZvU6Ccy4B" - } - }, - "idempotencyKey": "bfdb8fe9-3d93-4322-bf16-077c9566050d" - } - } -} -``` - - \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/02-mutations/07-beam-mutations.md b/versioned_docs/version-2.x/03-api-reference/02-mutations/07-beam-mutations.md deleted file mode 100644 index 9b29227..0000000 --- a/versioned_docs/version-2.x/03-api-reference/02-mutations/07-beam-mutations.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -title: "Beam" -slug: "beam" -description: "Perform Beam mutations in the Enjin API to manage QR code activations, enabling blockchain asset distribution via QR code scans." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - -:::tip Beam Endpoints -- **Testnet:** `http://platform.canary.enjin.io/graphql/beam` -- **Mainnet:** `http://platform.enjin.io/graphql/beam` -::: - -This is a detailed reference guide that explains the most commonly used operations. - -## CreateBeam - -The `CreateBeam` mutation is used to create a new Enjin Beam distribution campaign. Enjin Beam is a method for distributing tokens via QR codes. This mutation sets up the details and parameters of the Beam campaign. - - - -```graphql -mutation CreateBeam { - CreateBeam( - name: "My Beam" - description: "This is a test beam." - image: "https://assets-global.website-files.com/60f57c496975b84c29335fb7/60f58bd888a6e86e3ff69551_Enjin.svg" - start: "20230101" - end: "20240101" - collectionId: 7153 - tokens: [ - {tokenIds:"1..10" tokenQuantityPerClaim:1 claimQuantity:1 type:MINT_ON_DEMAND} - ] - ) -} -``` - - -```json -{ - "data": { - "CreateBeam": "cd6443f822cf6e89c03edd4f6fbc6e55" - } -} -``` - - - -## ClaimBeam - -The `ClaimBeam` is used to claim a specific Enjin Beam using a unique code. Enjin Beam is a method for distributing tokens, and this mutation allows users to claim their allocated tokens by providing the necessary code and cryptographic signature. - - - -```graphql -mutation ClaimBeam { - ClaimBeam( - code: "cd6443f822cf6e89c03edd4f6fbc6e55" - account: "cxMsNPRk7Ek5V76NC4o2HTBrnxcUnxLA9btuKPcuPkmYi84Ts" - signature:"0x1d670c35a76efa984fa2bd655d21eb28af0ee470e359848d2105845e170bb589fd6b7c2cbe97c9acce1eb205d4ade39cae75c619230cf44b64b25ec8677d7f0f" - cryptoSignatureType:SR25519 - ) -} -``` - - -```json -{ - "data": { - "ClaimBeam": true - } -} -``` - - - -## UpdateBeam - -The `UpdateBeam` mutation is used to modify the properties of an existing Enjin Beam. It allows you to update specific attributes of a Beam, such as its name, to ensure that the campaign remains accurate and adaptable. - - - -```graphql -mutation UpdateBeam { - UpdateBeam( - code: "ca8524349abab1cc20994841a8e0f175" - name: "My beam UPDATED" - ) -} -``` - - -```json -{ - "data": { - "UpdateBeam": true - } -} -``` - - - -## DeleteBeam - -The `DeleteBeam` mutation is used to delete an existing Enjin Beam from the system. It allows you to remove a Beam and all of its associated data based on its unique identifier code. - - - -```graphql -mutation DeleteBeam { - DeleteBeam(code: "cd6443f822cf6e89c03edd4f6fbc6e55") -} -``` - - -```json -{ - "data": { - "DeleteBeam": true - } -} -``` - - diff --git a/versioned_docs/version-2.x/03-api-reference/02-mutations/_category_.json b/versioned_docs/version-2.x/03-api-reference/02-mutations/_category_.json deleted file mode 100644 index ef4a0fc..0000000 --- a/versioned_docs/version-2.x/03-api-reference/02-mutations/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Mutations (Write)" -} - \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/03-api-reference.md b/versioned_docs/version-2.x/03-api-reference/03-api-reference.md deleted file mode 100644 index d2b9c57..0000000 --- a/versioned_docs/version-2.x/03-api-reference/03-api-reference.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: "API Reference" -slug: "/api-reference" ---- -The Enjin API provides a robust GraphQL interface for interacting with the Enjin Platform. This guide will demonstrate how to explore available queries and mutations using the GraphiQL Playground. - -## Exploring the GraphiQL Playground - -The GraphiQL Playground is an interactive environment where developers can explore the full range of API functionalities and build queries/mutations. Here’s how you can navigate and utilize this tool effectively. - -### Accessing the GraphiQL Playground - -You can access the GraphiQL Playground for both Testnet and Mainnet environments: - -- **Testnet**: [Core Operations](https://platform.canary.enjin.io/graphiql) -- **Mainnet**: [Core Operations](https://platform.enjin.io/graphiql) - -### Navigating the Documentation Explorer - -#### Step 1: Open the Documentation Explorer - -- Start by opening the GraphiQL Playground and click the `Show Documentation Explorer` button located at the top left. This will display a panel listing all available queries, mutations, and types. - -![Show Documentation Explorer](/img/getting-started/show-documentation-explorer.gif) - -#### Step 2: Search for a Specific Mutation - -- In the Documentation Explorer, you can use the search input to find a specific mutation. For example, to find the "CreateToken" mutation, type "Create" into the search bar. This helps narrow down the list to relevant operations. - -![Search mutations or queries](/img/getting-started/search.gif) - -:::tip Tip: quick Access to Documentation -If you already have a query or mutation in your builder, you can quickly open its documentation page by clicking on it while holding the `Ctrl` button. This will navigate you directly to its details in the Documentation Explorer. -::: - -#### Step 3: Explore the Mutation Details - -- Click on the "CreateToken" mutation to view detailed information about it. You will see: - - **Overview**: A description of what the mutation does. - - **Return Type**: The type of data it returns, such as a "transaction". - - **Arguments**: A list of arguments it accepts, with descriptions and data types. - -![Explore mutation, query, or type](/img/getting-started/explore-mutation-query-type.gif) - -### Understanding Arguments and Types - -#### Example: Creating a Token with Initial Supply of 10 - -- For instance, if you want to create a token with an initial supply of 10, you will need to focus on the `params` argument. This argument is of type "CreateTokenParams", a complex type. - -

- Example of exploring an argument -

- -- Click on `CreateTokenParams` to explore its fields: - - **tokenId**: Of type "EncodableTokenIdInput", which specifies the token identifier. - - **initialSupply**: Of type "BigInt", which you would set to 10. - - **cap**: Of type "TokenMintCap", which may define the maximum supply (optional). - -

- Example of exploring a complex type argument -

- -#### Constructing Your Mutation - -- Using the information from the Documentation Explorer, you can construct a mutation to create a token. Here is an example: - -```graphql -mutation { - CreateToken( - recipient: "efRecipientAddress" - collectionId: "12345" - params: { - tokenId: { integer: 1 } - initialSupply: 10 - } - ) { - id - method - state - } -} -``` - -- This mutation creates a new token with an initial supply of 10 in the specified collection. Adjust the parameters as needed for your application. - -:::tip Not sure how to construct the query/mutation? -Check out our [GraphQL Query Builder Guide](/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md#graphiql-playground) for detailed instructions on building queries and mutations. -::: - -#### Conclusion - -By using the Documentation Explorer in the GraphiQL Playground, you can efficiently navigate and understand the Enjin API's queries, mutations, and field types. diff --git a/versioned_docs/version-2.x/03-api-reference/03-websocket-events.md b/versioned_docs/version-2.x/03-api-reference/03-websocket-events.md deleted file mode 100644 index f2325ac..0000000 --- a/versioned_docs/version-2.x/03-api-reference/03-websocket-events.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: "Websocket Events" -slug: "websocket-events" ---- -:::info Websocket URLs -Enjin Platform Cloud **Canary**: `wss://ws-us2.pusher.com:443/app/76b7604dabc23c62be75?protocol=7`\ -Enjin Platform Cloud **Mainnet**: `wss://ws-us2.pusher.com:443/app/02cbd93e7842fb1db299?protocol=7` -::: - -The Enjin Platform emits events that you can listen to for various operations. -For example, whenever a transaction is issued by your wallet daemon, an event is emitted via Websocket. - -Listening to events is especially useful when awaiting for a transaction to be approved on-chain. - -Platform events are emitted to the platform Websocket endpoint, in various channels. -For example, we can listen to events related to account address `cxLU94nRz1en6gHnXnYPyTdtcZZ9dqBasexvexjArj4V1Qr8f` by subscribing to the channel `0x5a6aae294416f3e875d9a8975658905002cfd3e5e64105d76296c4b0adbfd77e`, which is the account's public key. - -:::note Note on Address Formats in Platform Events -Please note that all account addresses included in platform events, such as channel names or any addresses within the event payload, are formatted as **public keys**.\ -For example, in a "Transfer" event, the "from" account address appears in its public key format.\ -If needed, you may use the [Account Format Transform tool](https://matrix.subscan.io/tools/format_transform) to convert between **SS58-encoded address format** (e.g., "cx...123" for the Canary Matrixchain or "cn...123" for the Canary Relaychain) and public key format. -::: - -```json -{ - "event":"pusher:subscribe", - "data":{ - "channel":"0x5a6aae294416f3e875d9a8975658905002cfd3e5e64105d76296c4b0adbfd77e" - } -} -``` - -![Example banner](/img/api-functions/Pusher.gif) - -To get the full list of events and the channels they are broadcasted in, send the `GetEvents` query: - -```graphql -query { - GetEvents -} -``` diff --git a/versioned_docs/version-2.x/03-api-reference/04-important-arguments.md b/versioned_docs/version-2.x/03-api-reference/04-important-arguments.md deleted file mode 100644 index d95b179..0000000 --- a/versioned_docs/version-2.x/03-api-reference/04-important-arguments.md +++ /dev/null @@ -1,434 +0,0 @@ ---- -title: "Arguments" -slug: "important-arguments" -description: "A quick reference for some important arguments used in the Enjin API Schema." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; - -:::info Please note: This is an introductory reference -For the most up-to-date information, refer to the [API Reference](/03-api-reference/03-api-reference.md).\ -🚧 The information provided in this section cannot be programmatically updated and may be subject to inconsistencies over time. -::: - -This is a detailed reference guide that explains most common arguments within the Enjin API Schema. - -## id - -The `id` argument is a unique identifier used to specifically identify a query or mutation that occurred on the Enjin Platform. - -It helps pinpoint a particular operation when you want to reference it or perform actions related to that operation. - -This identifier is typically generated by the Enjin API and serves as a reference point for interacting with specific operations. - -## transactionId - -A convenient way to rapidly reference blockchain transactions in a sequential order. - -It is structured in the format `xxxxxx-y`, where `xxxxxx` denotes the block number, and `y` indicates the position of the transaction within that block. - -## transactionHash - -The "transactionHash" argument in GraphQL serves as a unique identifier for a particular blockchain transaction. - -It is generated using a cryptographic hash function, guaranteeing that each "transactionHash" is entirely distinct and cannot resemble an identifier from a different blockchain network. - -## idempotencyKey - -A special key that prevents the same operation from being accidentally carried out more than once. It ensures that if you send the same request multiple times, it has the same effect as sending it once, avoiding unintended duplicates. - -## verificationId - -A unique identifier that is returned once you verify a user's ownership over a specific wallet. - -You will need to use the `verificationId` to find a wallet address once it's been verified but you can also use it to find other important information regarding the wallet. - -## after - -Used as a cursor to specify a starting point for fetching data in a paginated list. - -It helps you retrieve data after a specific item or position in the list. - -## first - -Used to specify the number of results or items you want to return per page when dealing with paginated data. - -Simply provide it with a numeric value that represents the desired number of results you want to retrieve in a single page. - -## account - -Your "Enjin Blockchain account" that acts like your digital identity on the blockchain. - -It allows you to interact with the blockchain, such as sending or receiving tokens. - -Think of it as your account on the blockchain network. - -### address - -A human-readable representation of your Enjin Blockchain account. - -Users typically interact with and see the address as their primary identifier in wallets and marketplaces. - -It's often in the form of a long alphanumeric string, making it easier for people to send you tokens. - -Your address is derived from your public key and serves as a more user-friendly way to identify your account. - -### publicKey - -A cryptographic key associated with your Enjin Blockchain account. - -It serves as your unique identifier on the blockchain. - -When someone wants to send you tokens or verify your transactions, they use your public key to do so. It's like your digital fingerprint. - -## hashes - -Allows you to filter blockchain transactions based on their unique transaction hashes. - -Transaction hashes are cryptographic representations of individual transactions on the blockchain, ensuring their immutability and uniqueness. - -## numbers - -Used for filtering transactions by block numbers, allows you to specify the block numbers from which you want to retrieve transactions. - -This is useful when you want to narrow down and retrieve transactions from specific blocks on the blockchain. - -## collectionId - -A defining identifier for token collections on the Enjin Blockchain. It is automatically generated by the blockchain when a new collection is created. - -Token collections play a pivotal role in organizing and managing related tokens, simplifying their accessibility and interaction within wallets and on marketplaces. - -## acknowledgeEvents - -Controls whether to automatically acknowledge events that were broadcast to the blockchain but have not yet been acknowledged by the blockchain itself. - -When events are acknowledged before finalization on the blockchain, the operation's `result` is set as `EXTRINSIC_FAILED`, indicating a failure in the blockchain's processing. - -## tokenId - -Used to request specific information about a particular token on the Enjin Blockchain. - -It allows you to retrieve details, properties, and attributes of the token identified by its unique token ID. - -### integer - -A straightforward numerical identifier for a token. It's a simple and efficient way to represent tokens. - -You might choose `integer` when you want a basic, numeric representation of tokens without additional complexity. - -It's useful for scenarios where simplicity and efficiency are more important than encoding additional information in the ID. - -### stringId - -Derived from a string, such as a name or label. It you to choose human-readable representations of tokens. - -You might choose `stringId` when you want to represent tokens with user-friendly names or labels. - -It's useful for mapping easily recognizable names. - -### hash - -A `hash` is created using a cryptographic hash function, ensuring uniqueness and collision resistance. - -It's suitable when uniqueness across collections is critical. - -It's often used in cases where tokens must be resistant to duplication or tampering. - -### ERC1155 - -Designed to facilitate seamless conversion between tokens Ethereum Virtual Machine (EVM) chains and tokens on Enjin Blockchain. - -Particularly useful for projects that require interoperability between blockchain ecosystems. - -It enables tokens to be converted from one chain to another while retaining their essential properties, making it a valuable tool for cross-chain token management and exchange. - -#### index - -A 16-bit integer that represents a specific instance or variation of a token type within an ERC-1155 contract. - -It is used in combination with the base token ID to uniquely identify individual tokens of the same type within the contract. - -#### tokenId - -A unique identifier used within the ERC-1155 token standard. - -It combines two components: a 16-bit hexadecimal base token ID and a 16-bit integer index. - -When these components are combined, they create a 128-bit integer that serves as a distinct identifier for a specific token within an ERC-1155 contract. - -## method - -Enables precise and efficient retrieval of transaction-specific method. - -This targeted approach offers a clear and detailed understanding of the exact actions performed within a transaction, simplifying debugging, security auditing, and user interaction. - -It streamlines data retrieval by focusing on the essential method details, making it a powerful tool for developers, auditors, analysts, and end-users to gain insights, enhance security, and improve the overall user experience in blockchain applications. - -## signedAtBlock - -Specifies the exact block at which a transaction was signed on the blockchain. - -It serves as a historical reference point, indicating precisely when the transaction was initiated and digitally signed within the blockchain's timeline. - -This information is particularly useful when there is a need find a transaction within a block, verify the authenticity of a digital signature, or ensure compliance with timestamp-related requirements. - -It enables users, developers, and auditors to track the temporal context of blockchain interactions, offering a clear historical record that can be crucial for auditing, legal, regulatory, and security purposes. - -## state - -Used for tracking and monitoring the progress of Enjin Platform queries and mutations, offering insights into their current status and history. Here are the various states and why they are useful: - -- **`ABANDONED`:** Indicates that the operation couldn't be sent, often due to encoding issues or insufficient funds. -- **`PENDING`:** Represents a newly created operation that is awaiting approval by a wallet. -- **`PROCESSING`:** Signifies that the Enjin Wallet Daemon has picked up the transaction and is actively processing it. -- **`BROADCAST`:** Indicates that the transaction has successfully sent the transaction to the Enjin Blockchain. -- **`FINALIZED`:** Denotes that the transaction has been successfully included in a block on the blockchain. Although, it doesn't specify whether the on-chain transaction succeeded or failed. That data can be queried by using the `result` field. - -## result - -Indicates whether a transaction has succeeded (`ExtrinsicSuccess`) or failed (`ExtrinsicFailed`). - -This is invaluable for transaction verification and error handling. - -It offers transparency and user confidence in blockchain interactions, aids in security audits and compliance checks, and enables automated processes to make decisions based on transaction outcomes. - -This single piece of information is pivotal for ensuring the reliability, integrity, and functionality of blockchain-based applications and systems, making it an essential component in blockchain transaction processing. - -## externalId - -You can utilize the `externalId` argument to establish a connection between an account in database and an Enjin Platform Account. - -It's worth noting that you have control over setting a user's `externalId`, so it's advisable to choose an identifier that aligns with your existing database, making "`externalId`" a valuable and versatile option for processing requests. - -## signature - -Allows for message authentication, ensuring that data remains unaltered during transmission, and guarantees the integrity of blockchain-stored data. - -Additionally, `signature` serves as a means of verifying user identity and authorization, providing non-repudiation, enhancing security, and confirming the legitimacy of blockchain transactions. - -Overall, this argument is indispensable for maintaining trust, security, and data reliability in cryptographic and blockchain environments. - -## uuids - -You would use the "uuids" argument in an argument to acknowledge and remove specific events from a cache, ensuring cache integrity and efficient system operation by eliminating unnecessary data. - -## expiration - -You use the `expiration` argument when authorizing another account to manage tokens from a specific collection for a limited time, ensuring that the approval automatically expires at the specified block number, providing control and security in scenarios involving NFTs or tokenized assets without a permanent transfer of ownership. - -## operator - -Use the `operator` argument to grant specific accounts the authorization to manage and operate a token collection, allowing for delegated token management without transferring ownership. - -## signingAccount - -Use the `signingaccount` argument to specify the account responsible for signing and authorizing a particular action or transaction, ensuring security, accountability and traceability in the process. - -## simulate - -You would use the `simulate` parameter set to `true` when you want to test and validate the outcome of a transaction without actually executing it on the blockchain network. - -It provides a valuable way to safely experiment with transactions and prevent unintended consequences, especially in scenarios where accuracy and precision are critical. - -## recipients - -You can use the **recipients** parameter for transferring or minting tokens, to specify the accounts to which the tokens will be transferred, enabling targeted and controlled token transfers to designated recipients. - -## attributes - -use on-chain attributes to assign and manage metadata, enabling detailed and comprehensive token metadata definition, which is valuable for providing rich and descriptive information about the token on the blockchain. - -### key - -A "key" serves as the identifier or label for a specific attribute. - -### value - -A "value" represents the corresponding information or data associated with that attribute. - -## hasRoyalty - -Configures royalties for each marketplace sale of this token. -Royalties are effectively enforced by the core blockchain, ensuring you receive your entitled earnings whenever the token is involved in transactions, providing a transparent and automated system for royalty distribution. - -- #### beneficiary - - - The recipient or entity designated to receive a portion of the proceeds from token sales - -- #### percentage - - - Represents the specific portion or share of the transaction value that is allocated to the beneficiary. - -## isCurrency - -This concept is used by the marketplace to determine what assets are allowed to be listed, and which side of the listing is charged fees. - -## cap - -Employed to limit the maximum supply of tokens, creating scarcity and rarity by setting an upper bound on the number of tokens that can ever be created. - -- #### SUPPLY - - - A fixed supply model where the cap amount sets the maximum tokens that can exist in circulation. Burned tokens can be re-minted. - -- #### COLLAPSING_SUPPLY - - - A dynamic supply model where the cap amount defines the total tokens that can ever be minted. Burned tokens cannot be re-minted. - -- #### SINGLE_MINT (DEPRECATED) - - - Allows the token to be minted only once. Functions as a collapsing supply model with the cap amount set to the initial supply. - -- #### INFINITE - - - Allows unlimited token creation with no supply cap. - -### type - -defines the token's minting behavior, where `INFINITE` allows unlimited minting, `SINGLE_MINT` enables minting of a single token, and `SUPPLY` specifies a fixed supply of tokens. - -## amount - -Represents a numerical value associated with the quantity of a token, such as the quantity available, the total supply, or the number of tokens to be minted, depending on the context of its use. - -## freezeState - -Allows you to define the duration of the suspension of transferability, with `PERMANENT` indicating a long-term freeze, `TEMPORARY` signifying a short-term suspension, and `NEVER` indicating that the token or collection will never be frozen, providing flexibility in asset management. - -## initialSupply - -Specifies the quantity of tokens that will be minted and delivered to the designated recipient. - -## listingForbidden - -Setting `listingForbidden` to true indicates that the token is restricted from being listed on a marketplace, providing control over its availability and preventing it from being traded or displayed on external platforms. - -## unitPrice - -The `unitPrice` of a token is critical as it determines the cost structure, ENJ backing, and accessibility of the token, with the added significance that it can be increased but never decreased, requiring alignment with the token's distribution goals. - -## signingAccount - -The "signingAccount" parameter should be configured with the address of the wallet that owns the token, ensuring that the actions and transactions associated with the token are appropriately authorized and executed by the correct account. - -## skipValidation - -The `skipValidation` option, when set to true, allows for the bypassing of validation checks, which should be exercised with caution and only in scenarios where custom validation or specific requirements necessitate the bypassing of standard validation rules. - -## continueOnFailure - -The `continueOnFailure` parameter, when set to true, indicates that the system should proceed with the transaction even if it encounters a failure or error during execution, offering a graceful handling of errors and allowing the transaction to continue despite issues. When set to false, any failure during execution will result in the transaction being halted and not proceeding further. - -## keepAlive - -When conducting Enjin Coin (ENJ) transfers, utilizing the "keepAlive" option with a value of true is advisable if you intend to preserve the sender's Enjin Coin Account, even when its balance drops below the minimum threshold of 0.1 ENJ. - -It's important to recognize that Enjin Coin Accounts and Token Accounts are distinct entities, meaning that a wallet can still hold tokens and NFTs even if the Enjin Coin Account is removed. - -## source - -The `source` parameter refers to the address from which the tokens will be transferred, representing the ownership address of the tokens and the origin of the transfer. - -## mintPolicy - -The "mintPolicy" parameter allows you to define the minting policy for tokens within a collection, with the following sub-parameters: - -- #### forceCollapsingSupply - - - Determines whether the tokens in this collection will be minted as **CollapsingSupply** types. This supply type allows the collection owner to mint new tokens as long as the token's circulating supply does not exceed it's max supply. Burning tokens reduces the max supply, meaning that in collapsing supply type, burned tokens cannot be re-minted. - -- #### maxTokenCount - - - Specifies the maximum number of tokens that can be issued within this collection, setting an upper limit on the total token count. - -- #### maxTokenSupply - - - Sets the maximum quantity of each unique token within this collection that can be minted, establishing an upper limit on the available supply for individual tokens. - -## marketPolicy - -Configures default marketplace royalties for all tokens in the collection. - -- #### royalty - - - A default royalty percentage for all tokens in the collection. This can be overridden on a per-token basis. - -## explicitRoyaltyCurrencies - -The `explicitRoyaltyCurrencies` field within a collection serves the purpose of specifying which currencies are permitted to be accepted as royalties. - -By including specific currencies in this field, a collection restricts the acceptance of royalties to only those currencies, ensuring that any listing created that intends to pay royalties in an unaccepted currency will result in a failed listing creation process. - -This feature enhances control and compliance in managing royalties within the collection. - -## freezeType - -"Freezing" refers to the process of temporarily suspending the transferability of a collection or a specific token. - -The `freezeType` parameter offers flexibility in specifying what can be frozen within the blockchain system, with the following options: - -- "COLLECTION": Indicates that an entire collection of tokens can be frozen. - -- "COLLECTION_ACCOUNT": Allows for the freezing of an entire collection account. - -- "TOKEN": Permits the freezing of individual tokens. - -- "TOKEN_ACCOUNT": Allows for the freezing of an entire token account. - -## markAsProcessing - -The "markAsProcessing" action involves retrieving a list of newly pending transactions and subsequently marking them as "processing," indicating that they are currently being handled or executed within the blockchain system. - -## owner - -The term "owner" can be employed in various contexts, depending on the specific action being performed. - -For example, when unapproving a collection, "owner" refers to the account from which the collection approval will be removed, signifying the entity or account that holds ownership or control over the approval status of the collection. - -# Beam - -## TokenIds - -Specifies a list of integer s claimable in the beam. A Beam Claim will be generated for each provided ID. -Provide IDs as strings, using commas to separate multiple entries. Supports single IDs (e.g., "1"), ranges (e.g., "6..8"), or a combination (e.g., ["1", "6..8", "10"] includes IDs 1, 6, 7, 8, and 10). - -## tokenQuantityPerClaim - -Specifies how many token units will be minted or transferred for each Beam Claim. - -## claimQuantity - -Specifies the number of Beam Claims to create for each provided token ID. Defaults to 1. - -## type {#beam-claim-type} - -Specifies how tokens are delivered when the beam is claimed: - -- #### TRANSFER_TOKEN - - - Transfers existing tokens to the recipient. - -- #### MINT_ON_DEMAND - - - Mints tokens directly to the recipient. - -## claimLimit - -Specify the number of times each user can claim the beam. - -## flags - -Specifies the type of beam behavior to modify. Available options include: - -- #### PAUSED - - - Temporarily prevents claims from being processed. - -- #### SINGLE_USE - - - Enables the use of single-use claim codes for distributing specific tokens from the beam. - -- #### PRUNABLE - - - Allows claim records for expired beams to be deleted from the database after a set period. \ No newline at end of file diff --git a/versioned_docs/version-2.x/03-api-reference/_category_.json b/versioned_docs/version-2.x/03-api-reference/_category_.json deleted file mode 100644 index 950677a..0000000 --- a/versioned_docs/version-2.x/03-api-reference/_category_.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "label": "API Reference" -} \ No newline at end of file diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/01-validator-staking.md b/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/01-validator-staking.md deleted file mode 100644 index 410ab5c..0000000 --- a/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/01-validator-staking.md +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: "Validator Staking" -slug: "validator-staking" ---- -This is an overview of staking on Enjin Relaychain, known as liquid staking. - -:::info Staking Interfaces -- **[NFT.io](https://nft.io):** Use NFT.io for a simplified experience. It provides a convenient, user-friendly interface to manage your stake and nomination pools. -- **[Enjin Console](https://console.enjin.io/)**: For advanced features and the full user interface referenced in this document, use [console.enjin.io](https://console.enjin.io/). - - *Note:* Ensure you connect to the **Enjin Relaychain** network in the top left corner, as staking takes place on the Relaychain. -::: - -## Concepts - -- **Direct staking -** A user stakes directly to one or more validators and is paid directly by them. -- **Nomination Pool (indirect staking) -** One account stakes funds that represent a group of users. This account handles the nominating and receives the rewards. -- **Liquid staking -** a token is received that represents the user's stake. The staked token can be used (is liquid) while the user is staked. It is minted when the user stakes and burned when they unstake. It can also be exchanged for the real token. -- **Validator -** Provides security for the network and receives rewards for doing so. On Enjin, these rewards are distributed to nomination pools. (A list of active validators can be seen on [Subscan](https://enjin.subscan.io/validator).) -- **Nominator -** The account that chooses a validator to stake with. In the case of Enjin, this is the nomination pool's account. -- **Commission -** A percentage of the rewards taken for offering a service. This can be either or both: a commission set by a validator, for their contributions to progressing and securing the network; and/or a commission set by a nomination pool owner, for having created the pool for which the active validator(s) were selected and the user(s) of the pool benefited. - -## Nomination Pools vs Direct Staking - -On Enjin, we use nomination pools to handle staking due to the increased simplicity and lower minimums. Another type of staking supported on substrate is direct staking, where a user stakes directly to a validator, but it has several disadvantages. - -### Nomination Pools with liquid staking - -With nomination pools, there is one account that multiple users put their funds into. This account acts as the nominator and receives the rewards. The liquid token, sENJ, is used to represent a user's stake in the pool. - -- **Low minimum (1 ENJ) -** pools can have lower minimums per user because all of the pool's funds are staked together -- **Easy to use** - users only need to choose a pool, no need to choose validators -- **Receive liquid token -** a liquid token, sENJ, is received that represents the user's stake. This can be transferred or even exchanged for ENJ -- **Immediate exit -** due to the liquid token, the user can exit immediately by exchanging their sENJ for ENJ - -

- -

- -### Direct Staking - -:::note -Direct staking is not supported on Enjin. This information is just for comparison. -::: - -- **High minimum -** direct staking has a high minimum balance. On polkadot, it's 250 DOT. -- **Must choose validators -** each validator must be manually chosen. This can be difficult for new users. -- **Staked token is locked -** the staked token cannot be used while staked -- **Stash / controller accounts -** multiple accounts must be created, increasing complexity -- **28 days unbonding period -** users must wait 28 days after unbonding to receive their funds - -

- -

- -## Tokens Overview - -There are three tokens relevant to staking: - -- **ENJ -** ENJ is the native token of the Enjin network. It is the token staked in the pool. -- **sENJ -** sENJ is the liquid token that represents staked ENJ. It is minted when ENJ is staked and burned when ENJ is unstaked. It can also be exchanged for ENJ through the `stakeExchange` pallet. -- **Degen Token -** The degen token is an NFT in a special collection. It is required to create a pool and is also used to manage the pool. The owner of this NFT can optionally receive a commission. Each token is only usable in one pool at a time. - -![](/img/components/enjin-relaychain/3.png) - -## Deposit - -A deposit is reserved when the pool is created. This deposit will be released to the holder of the degen token when the pool is destroyed. Here's an example: - -1. Alice creates a pool with degen 3 and 5000 ENJ - 1. ENJ is locked in a pool as deposit - 2. Degen 3 is associated with the pool - 3. Alice receives commissions -2. Alice transfers degen 3 to Bob - 1. Bob begins receiving commissions - 2. Bob can now manage the pool -3. Bob destroys the pool and everyone withdraws - 1. Bob gets the 5000 ENJ deposit because he holds the degen - 2. Bob would actually get slightly more than 5000 ENJ because the deposit is staked, so he would get the rewards as well - -![](/img/components/enjin-relaychain/4.png) - -## ENJ & sENJ - -Here's an example to show the relationship between ENJ and sENJ. The important thing to understand is that although the number of sENJ does not change as rewards are distributed, its value increases because it represents a proportion of the total ENJ in the pool. - -![](/img/components/enjin-relaychain/5.jpg) - -## Payouts - -Reward payouts are automated by validators, eliminating the need for manual intervention. However, understanding the distribution logic is helpful: - -1. **Reward Collection:** Staking rewards are collected from the validator into the reward account for the current era. -2. **Reward Distribution:** Rewards are distributed for the *previous* era (one era lag) to ensure all validator rewards are fully collected first. This stage involves: - 1. Sending the commission to the Degen token holder. - 2. Staking (reinvesting) the pool's remaining reward balance to maximize future returns. - -![](/img/components/enjin-relaychain/8.png) - -## Extrinsics Overview - -Here's an overview of the extrinsics (operations) in pallet-nomination-pools. - -### Administration - -These extrinsics are for creating or modifying pools and require special permissions. Some must be called directly by the owner of the degen NFT, and a few can be called if assigned to a specific role. - -- **`create`** - creates a new pool. Requires the degen NFT and the deposit. -- **`chill`** - stops the pool from receiving rewards. -- **`destroy`** - puts the pool into the destroying state -- **`nominate`** - nominates validators for the pool -- **`mutate`** - makes changes to the pool - -### Members - -These are the extrinsics that will be called by users (members) of the pool. - -- **`bond`** - bonds ENJ to the pool. The user receives sENJ. -- **`unbond`** - Unbonds sENJ. The user burns sENJ. -- **`withdraw_unbonded`** - The ENJ is received from unbonding after the unbonding period has passed. - -### Permissionless - -These extrinsics can be called when the pool is in the `Destroying` state, by anyone. -* **`unbond_deposit`** - Unbonds the deposit. Only callable after the pool is destroyed and all members have left the pool. -* **`withdraw_deposit`** - Withdraws the deposit. Can only be called after the deposit is unbonded. The pool will be destroyed (deleted) after this is called successfully. \ No newline at end of file diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/02-enjin-relaychain.md b/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/02-enjin-relaychain.md deleted file mode 100644 index 6ab0d13..0000000 --- a/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/02-enjin-relaychain.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: "Enjin Relaychain" -slug: "../enjin-relaychain" -description: "Layer 0: The security layer of the Enjin Blockchain." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; - -## What is Enjin Relaychain? - -Enjin Relaychain is a decentralized and scalable network built for digital assets and non-fungible tokens (NFTs). It is based on Polkadot and uses the open-source Substrate framework to provide a comprehensive and efficient ecosystem for NFT creation. With superior functionality, speed, and low costs, Enjin Blockchain is designed to enhance the creation and management of digital assets in a secure and reliable manner. - -

- -

- -## Benefits of the Enjin Relaychain: - -1. Scalability: Enjin Relaychain is built to be highly scalable, enabling a significant increase in transaction throughput and enhanced performance for NFT projects, games, brands, and communities around the globe. This scalability is achieved through a unique architecture that leverages a multi-chain approach, allowing for parallel processing of transactions, reducing bottlenecks, and optimizing the use of resources. -2. Customizable Matrix Chains: Enjin Relaychain features a primary Relay chain and several Matrix Chains, which can be customized to meet the specific needs of different use cases. The Relay chain is optimized for security, network synchronicity, and block validation, while the Matrix Chains are specialized blockchains that can prioritize speed, scalability, and cost efficiency. By offloading security and integrity to the Relay chain, Matrix Chains can evolve and adapt to changing market demands without compromising their unique features or properties. This architecture provides the flexibility and agility needed for NFT projects, games, brands, and communities to innovate and thrive. -3. Predictable Scalability and Costs: Enjin Blockchain takes a different approach to scalability and costs than other blockchain networks by creating an interoperable network of Matrix Chains with predictable scalability and costs. Unlike the auction-based parachain lease model used in Polkadot, Enjin's approach eliminates the hindrances and risks associated with parachain auctions and high development costs. This makes it easier and more cost-effective to launch and upgrade Matrix Chains, providing a predictable and reliable environment for developers and businesses to build on. -4. Streamlined Staking: Enjin Relaychain uses a nominated proof-of-stake mechanism on the Relaychain to maintain security. Staking is simplified through an intuitive user interface that is accessible directly from the Enjin Wallet. Users can easily allocate Enjin Coin to a curated nomination pool with just one click, removing the need for manually selecting individual nodes. The staked amount remains highly liquid and can be used within the Enjin ecosystem, without requiring it to be unbound from the nomination pool beforehand. This ensures the coin's utility and enables users to participate in securing the network seamlessly. -5. Inclusivity and Accessibility: Enjin Blockchain is founded on the principles of inclusivity and accessibility. It provides an opportunity for anyone to participate in network security and earn Enjin Coin rewards. With pre-configured packages, anyone can deploy a , Node, or Nomination Pool without the need for complex technical knowledge, making it easy for users to contribute to the network. This approach ensures that the network is open and accessible to all, promoting inclusivity and community involvement. -6. Seamless Integration with Enjin Products: Enjin Relaychain seamlessly integrates with popular Enjin products, such as the Enjin Wallet, NFT.io - NFT Marketplace, Enjin Platform, Enjin Beam, Enjin Web Wallet, Enjin Platform SDKs, and more. This provides a comprehensive and end-to-end integrated ecosystem, delivering a smooth experience for both users and developers. The integration ensures that all Enjin products leverage the benefits of the Relaychain, including scalability, low costs, and increased speed. - -Enjin Relaychain is a decentralized, multi-chain network that aims to provide a superior digital asset ecosystem for NFT projects, games, brands, and communities globally. Its nominated proof-of-stake staking structure guarantees regulatory compliance and security. Enjin Relaychain offers the following key features and benefits: - -- Scalable and customizable multi-chain network; -- Enhanced NFT creation capabilities; -- Predictable scalability and costs; - User-friendly staking; -- Inclusive and accessible for all users; -- Seamlessly integrated with Enjin products. - -Overall, Enjin Relaychain offers a secure and efficient platform for creating and managing NFT projects, games, and communities. Its features and benefits are designed to make the process of creating and managing digital assets more accessible, cost-effective, and efficient while ensuring regulatory compliance and security. diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/02-stake-exchange-pallet.md b/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/02-stake-exchange-pallet.md deleted file mode 100644 index c34536b..0000000 --- a/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/02-stake-exchange-pallet.md +++ /dev/null @@ -1,175 +0,0 @@ ---- -title: "Stake Exchange Pallet" -slug: "stake-exchange-pallet" ---- -:::info The Enjin Blockchain Console -Use [console.enjin.io](https://console.enjin.io/) to use the user interface referenced in this document. -::: - -## Introduction - -Stake Exchange Pallet enables users holding ENJ tokens to create offers for staking and allows users holding sENJ tokens to exchange them back to ENJ tokens. Each offer has preferences regarding the exchange rate and the specific type of sENJ tokens it can accept. It's important to note that sENJ tokens correspond to a different pool in the Stake Exchange Pallet. Users can select the offer that best suits their preferences and perform the token exchange accordingly. This user manual provides step-by-step instructions on how to use the pallet to stake ENJ tokens and exchange them for sENJ tokens. - -## Enjin Nomination Pools and Liquid Staking: - -Enjin Nomination Pools are an integral part of the Stake Exchange Pallet. They serve as pools that hold ENJ tokens, and issue sENJ tokens in return. Each sENJ type corresponds to a specific pool. - -Liquid Staking allows users to stake their ENJ tokens and receive sENJ tokens in return. These sENJ tokens represent the staked value of their ENJ tokens. By staking their ENJ tokens, users contribute to the security and operation of the Enjin network. - -Through the Stake Exchange Pallet, users holding sENJ tokens can easily convert them back to ENJ tokens by selecting the appropriate offer from the available options. Each offer within the pallet specifies the exchange rate and the type of sENJ tokens it accepts. Users can choose the offer that aligns with their preferences and execute the token exchange accordingly. - -By utilizing Enjin Nomination Pools and the Stake Exchange Pallet, users have the flexibility to stake their ENJ tokens for network participation and later convert them back to ENJ tokens when desired, providing liquidity and accessibility to their staked assets. - -## Glossary - -**sENJ Tokens**: The staked version of the ENJ tokens. These tokens are represented using the `StakedCollectionId` collections in the `pallet_multi_tokens` instance. - -**Exchange Rate**: The rate at which ENJ tokens can be exchanged for sENJ tokens and vice versa. - -**Liquidity Account**: An account used for managing liquidity. It is associated with a specific user and holds the liquidity for their offers. - -## Getting Started - -Ensure that you have access to an Enjin Relay Chain and have the necessary ENJ tokens to stake. The UI used in this guide can be accessed [here (*Canary Testnet)](https://console.enjin.io/?rpc=wss%3A%2F%2Frpc.relay.canary.enjin.io#/extrinsics) - -**Accessing the Pallet Functions:** -Open your Enjin Explorer and navigate to the Developer -> Extrinsics interface. Locate the "Stake Exchange Pallet" and click on it to access the available functions. - -## Creating an Offer - -- Click on the "Create Offer" function to start the process of staking your ENJ tokens and creating an offer. -- Enter the required parameters for the offer: - - Total : Specify the amount of ENJ tokens you want to exchange. - - Rate : Define the exchange rate for converting sENJ tokens to ENJ tokens. - - minAverageRewardRate : Define the minimum reward rate you would accept for the offer. -- Review the entered parameters and click "Confirm" to proceed. -- If successful, you will receive a confirmation message indicating that the offer has been created. Note the generated Offer ID for future reference. - -*** - -**_Note_** : `PoolReward` is calculated by dividing the ENJ bonded in the pool by the points in the pool. Lets consider these two examples - -Pool X (has not received any rewards): - -| Points | Total Bonded ENJ | Avg reward rate | -| ------ | ---------------- | --------------- | -| 1000 | 1000 | 1 | - -Pool Y : (received some rewards) - -| Points | Total Bonded ENJ | Avg reward rate | -| ------ | ---------------- | --------------- | -| 1000 | 1100 | 1.1 | - -Pool Z : (slashed) - -| Points | Total Bonded ENJ | Avg reward rate | -| ------ | ---------------- | --------------- | -| 1000 | 900 | 0.9 | - -*** - -**_Note_** : `minAverageRewardRate` is used to represent the minimum accepted reward rate of a token (or pool). The reward rate of the pool is calculated by dividing the ENJ bonded in the pool by the points in the pool. By setting this value, the user can ensure a minimum reward rate for all sENJ tokens used to fill this offer. Lets consider two examples below - -1. minAverageRewardRate set to 1.1 - -In this case, only Pool Y tokens (and any other pool where average reward rate is greater than 1.1) will be accepted. If a user with sENJ tokens from Pool Y tries to buy from this offer, they will see the `RewardRateOutOfRange` error. - -2. minAverageRewardRate set to 1 - -In this case, both Pool X and Pool Y tokens (and any other pool where average reward rate is greater than 1) will be accepted. If a user with sENJ tokens from Pool Z with a reward rate of 0.9 tries to buy from this offer, they will see the `RewardRateOutOfRange` error. - -So as the creator of the offer, it is in your best interest to keep this value as high as possible, so that the sENJ you will receive will reap the maximum rewards. Do keep in mind that there are practical limitations in how high this value can be set, for example if you set it to `3` (aka 3x reward for every token), no sENJ tokens maybe eligible to fill your offer since it is unlikely to have a pool/validator with that high reward rate. The ideal value to set here can be obtained by inspecting the current active pools, and setting a value that is high enough and accepts most pools. - -*** - -**_Note_** : The `minAverageRewardRate` is represented by u128, which allows for higher precision. Example : - -if its 1, (the pool should have a reward rate greater than or equal to 1) you would use 1000000000000000000 (1x) -if its 1.1, (the pool should have a reward rate greater than or equal to 1.1), you would use 1100000000000000000 (11.x) -if its 2 (the pool should have a reward rate greater than or equal to 2), you would use 2000000000000000000 (2x) - -To get the input ratio, you can multiply your desired ratio with `1000000000000000000`, -ex : `1.112 * 1000000000000000000 = 1120000000000000000` -This ratio means, the offer will only accept pool with a reward rate more than 1.112 - -*** - -**_Note_** : The `rate` is also represented by Perbill, this value will be used to determine how many `sENJ` will be required to get `ENJ`. Example : - -if its 1:1, (you need 1 sENJ to get 1ENJ) you would use 1000000000 (100%) -if its 0.9:1, (you need 0.9 sENJ to get 1ENJ), you would use 900000000 (90%) - -*** - -**_Note_** : The `tokenFilter` is used to control which pool tokens can be used to fill an active offer. TokenFilter can be configured to be one of these values - -1. All (all pool tokens are accepted, configs set on liquidity account still apply) -2. Whitelist (this allows you to whitelist pool tokens, only these tokens will be accepted, all others will be rejected). -3. Blacklist (this allows to block selected pool tokens, all other tokens will be accepted.) - -*** - -The below example creates an offer with a liquidity of 1000ENJ (1000000000000000000000), and offers a rate of 99% (990000000), and will accepts pools with a reward rate of 1.1 (1100000000000000000). The offer only accepts tokens from Pool ID 2. - -![](/img/components/enjin-relaychain/34.png) - -## Canceling an Offer - -- If you wish to cancel a previously created offer, click on the "Cancel Offer" function. -- Enter the Offer ID of the offer you want to cancel. -- Click "Confirm" to proceed with the cancellation. -- If successful, you will receive a confirmation message indicating that the offer has been canceled. - -![](/img/components/enjin-relaychain/35.png) - -## Configuring Liquidity Account - -:::note -Configuring Liquidity Account is only required if you create an offer aka if you provide liquidity to the pallet, you do not need to configure a liquidity account to buy tokens -::: - -- To configure your liquidity account, click on the "Configure Liquidity Account" function. -- The liquidity account configuration allows you to allow/disallow certain sENJ tokens from the offer created by your account. To allow an sENJ token, add the token to the whitelist and to disallow it, add it to the blacklist. These restrictions are enforced to all offers created by your account. - - Example configuration to disallow sENJ tokens with tokenId 210: - -![](/img/components/enjin-relaychain/36.png) - -## Withdrawing Liquidity - -- If you want to withdraw liquidity from an active offer, click on the "Withdraw Liquidity" function. -- Enter the Offer ID of the offer from which you wish to withdraw liquidity. -- Specify the amount of liquidity you want to withdraw. -- Click "Confirm" to proceed with the withdrawal. -- If successful, you will receive a confirmation message indicating that the liquidity has been withdrawn. - - Example : You created an offer with initial amount of 1000ENJ. If you withdraw liquidity of 400ENJ, then your updated offer will have a liquidity of 600ENJ. - -![](/img/components/enjin-relaychain/37.png) - -## Adding Liquidity - -- To add liquidity to an active offer, click on the "Add Liquidity" function. -- Enter the Offer ID of the offer to which you want to add liquidity. -- Specify the amount of liquidity you want to add. -- Click "Confirm" to proceed with adding liquidity. -- If successful, you will receive a confirmation message indicating that the liquidity has been added. - - Example : You created an offer with initial amount of 1000ENJ. If you add liquidity of 400ENJ, then your updated - offer will have a liquidity of 1400ENJ. - -![](/img/components/enjin-relaychain/38.png) - -## Buying Tokens - -- To exchange sENJ tokens for ENJ tokens, click on the "Buy" function. -- Enter the Offer ID of the offer from which you want to buy tokens. -- Specify the amount of ENJ tokens you want to receive. -- Enter the Token ID for the specific token you want to exchange. -- Click "Confirm" to proceed with the token exchange. -- If successful, you will receive a confirmation message indicating that the token exchange has been completed. You will also receive the exchanged ENJ tokens in your wallet. - -![](/img/components/enjin-relaychain/39.png) - -Happy staking and token exchanging! diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/03-slashing.md b/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/03-slashing.md deleted file mode 100644 index 9406408..0000000 --- a/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/03-slashing.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: "Slashing" -slug: "slashing" -description: "Slashing is a mechanism designed to ensure the integrity, security and reliability of the Enjin Blockchain network by penalizing validators for malicious or negligent behavior. This mechanism is crucial to maintaining trust and fairness within the network by discouraging actions that could compromise its safety or availability." ---- -## Causes of Slashing - -Slashing occurs when a validator commits an offense that goes against the Enjin Blockchain’s rules. Offenses that could lead to slashing on the Enjin Blockchain include: - -- **Double Signing (Equivocation):** A validator signs two different blocks at the same block height, potentially forking the blockchain and undermining the consensus mechanism. -- **Unresponsiveness:** A validator is offline or fails to produce blocks or participate in consensus rounds over a defined period, negatively impacting network performance and security. -- **Invalid State Transitions:** Proposing blocks containing invalid state transitions or transactions that do not comply with network rules can result in slashing. - -## Slashing Levels - -Slashing penalties are categorized into different levels depending on the severity of the offense: - -- **Minor Offense:** Includes brief periods of downtime or other non-malicious behavior. Penalties for minor offenses typically involve a small reduction in staked ENJ (0 - 1%). -- **Major Offense:** Encompasses more serious behaviors, such as repeated equivocation or failure to validate correctly over extended periods.These offenses carry harsher penalties, potentially resulting in moderately medium reductions in staked ENJ (1 - 10%) and possible exclusion from validator duties. -- **Critical Offense:** Involves malicious actions, such as double signing or producing invalid blocks, that directly threaten network security. Validators committing critical offenses may face severe penalties, including the loss of a substantial portion or all of their staked ENJ (10 - 100%) and immediate removal from the active validator set. - -## Slashing Process - -When a validator is found to have committed an offense, the slashing process is initiated: - -- **Detection:** The offense is detected by the Enjin Blockchain through automated mechanisms or reports from other validators and network participants. -- **Verification:** The detected offense is verified through consensus among the active validator set or via automated checks performed by the network. -- **Execution:** Once the offense is verified, the slashing is executed automatically by the network. The validator’s staked ENJ is reduced according to the severity of the offense, and the validator may be temporarily or permanently removed from the active validator set. - -### Slashed Tokens - -When slashing occurs, a portion of the validator’s staked ENJ tokens, including those of the nominators who have delegated their tokens to the validator, is forfeited. This forfeited amount is then transferred to the [Enjin Treasury](https://enjin.subscan.io/account/enD9wdMEaQa3MEDUc7dtsCC86JYGMN5JBE2NBRoMyC37dX4iA). The rationale for transferring slashed tokens to the Treasury rather than burning them or redistributing them as rewards is twofold: - -- **Reversal of Faulty Slashing:** Slashing penalties can be reverted by paying out from Treasury in cases where the slashing was deemed erroneous or unjustified. -- **Supporting Ecosystem Growth:** In the case of legitimate slashing, the tokens are redirected from malicious validators to support ecosystem development through the standard Treasury process, rewarding those actively contributing to the growth and health of the Enjin Blockchain. - -It’s important to note that slashing only affects active nominations for a given nominator. Slashes are not mitigated by having other inactive or waiting nominations. Furthermore, slashing penalties apply independently to each validator node operated by a validator, with each node being considered a separate entity for slashing purposes. - -#### Impact on Nominators - -Nominators who delegate their ENJ tokens to a validator are also at risk of slashing if the chosen validator commits an offense. The slashing penalty is distributed proportionally across the validator’s own stake and the stake of their nominators. Therefore, nominators should carefully choose validators with a proven track record of reliability and adherence to network rules. - -## Good Practices to Avoiding Slashing - -Validators on the Enjin Blockchain must follow best practices to avoid slashing penalties and ensure the security and reliability of nodes. You can refer to the [Security Best Practices for Validators](/02-guides/02-blockchain/02-running-nodes/02-operating-relaychain-validator/02-security-best-practices-for-validators.md) article for more information. - -## Conclusion - -Slashing on the Enjin Blockchain is an essential mechanism to maintain network security, integrity and reliability by discouraging malicious or negligent behaviors of validators. By understanding the causes, levels and process, validators and nominators can better navigate the network, manage risk and contribute to a healthy and secure ecosystem. diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/_category_.json b/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/_category_.json deleted file mode 100644 index 632c87b..0000000 --- a/versioned_docs/version-2.x/04-enjin-blockchain/02-enjin-relaychain/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Enjin Relaychain" -} - \ No newline at end of file diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md b/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md deleted file mode 100644 index 5c8c5ee..0000000 --- a/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md +++ /dev/null @@ -1,328 +0,0 @@ ---- -title: "MultiTokens Pallet" -slug: "multitoken-pallet" -description: "The utility pallets of the Enjin Blockchain." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; - -:::info The Enjin Blockchain Console -Use [console.enjin.io](https://console.enjin.io/) to use the user interface referenced in this document. -::: - -## What is the MultiTokens ? - -The Enjin Blockchain has a token standard called MultiTokens. The standard is compatible with matrixchains, parachains, parathreads and smart contracts, so it’s interoperable with the entire Enjin, Polkadot and Kusama ecosystem. - -**** are stackable, have a quantity and optional decimal places. An example of a multi-unit token is a twenty dollar bill - each bill is worth the same amount as another twenty dollar bill. - -![](/img/components/enjin-matrixchain/1.png) - -**s** are not interchangeable; each token has its own unique identifier. Examples of NFTs are original art, gaming characters and pets, numbered collectibles, and more. - -![](/img/components/enjin-matrixchain/2.png) - -**Grouped Tokens** are Multi-unit tokens or NFTs organized into logical sub-categories within a collection. This adds a structural layer between the collection and individual tokens, allowing for organized "folders" in apps like the Enjin Wallet and NFT.io. - -![](/img/components/enjin-matrixchain/3.png) - -:::info Partial Implementation -Token grouping is currently implemented at the blockchain level. While users can view and manage groups on NFT.io and view them in the Enjin Wallet, full Enjin Platform (API/UI) support is currently in development. -::: - -## Terminology - -- **Collection -** A group of tokens. Also holds data for those tokens and the policies that govern their behavior. -- ** -** An organizational layer within a collection used to categorize tokens. -- **Token -** A unique asset with a balance -- **Token Account -** A token account is stored in user account. It holds the account's states like its balance, freeze state, etc. -- **Policy -** Governs behavior for tokens in a collection -- **Attribute -** Metadata (strings/data) attached to a collection, group, or token. -- **Operator -** An account that operates on behalf of another account (transferFrom) -- **Approval -** Required for an operator to use an account -- **Freeze/Thaw -** If a collection, token, or account is frozen, it cannot transfer tokens -- **Descriptor -** Used to create something. For example, a CollectionDescriptor creates a Collection. - -## Collections - -A collection must be created before tokens may be minted. A collection is somewhat akin to an ERC-1155 smart contract - both and tokens can be created in a single collection, and its creator has certain privileges, such as minting new tokens or setting metadata for the collection and its tokens. - -The first 2000 Collection IDs are reserved for future system collections. Collections created on-chain through this start from ID 2001 and are sequentially created. - -A of 6.25 ENJ is required to create a collection. The deposit can be recovered by the collection owner if all tokens are burned and the collection is destroyed. - -![](/img/components/enjin-matrixchain/4.png) - -![](/img/components/enjin-matrixchain/5.png) - -![](/img/components/enjin-matrixchain/6.png) - -## Token Grouping - -Token Grouping adds an organizational layer between a **Collection** and its **Tokens**. It allows creators to categorize assets into logical "folders," significantly improving navigation and user experience. - -For example, a "Fantasy RPG" collection can group its tokens into "Weapons," "Armor," and "Potions." Within "Weapons," tokens could be further organized into "Swords" or "Axes." Without grouping, all tokens appear in a single, flat list, making specific items difficult to locate. - -### Functional Overview -* **Blockchain Level:** Groups are implemented directly on the MultiTokens pallet. -* **Attributes:** Like collections, groups can have their own attributes (metadata). -* **Organization:** In the Enjin Wallet and NFT.io, grouped tokens are displayed in folders rather than a random sorted list. - -### Implementation Status -Token grouping is currently in a **partial release** phase: -* **Enjin Wallet:** Users can view and navigate tokens organized into group folders. -* **NFT.io:** Users can view groups and manage them (manual or auto-grouping). -* **Enjin Platform:** UI and API support for managing groups via the Platform is currently in development. - -## Tokens - -Each token must belong to a Collection, and is created using the mint extrinsic. - -In the Enjin Blockchain there is no distinction between s and s. Each token is a that may be a multi-unit token or an NFT depending on its supply. A NFT is simply a token with a total supply of one. Additional constraints, like a cap, can be applied to a token at the time of minting to make sure the total supply never increases. - -![](/img/components/enjin-matrixchain/7.png) - -## How to Create a Collection - -A collection is created by using the `create_collection` extrinsic. The only required parameter is a descriptor which allows customizing the collection's behavior. The descriptor contains all of the policies and some additional settings. Some settings can be changed later using the `mutate_collection` extrinsic, but the policies currently cannot be changed, so think carefully before choosing the policies. - -By default, the policies are flexible, but you can make them more strict. For example, if you want to force a collection to only contain NFTs, you can set `max_token_supply` to `1` and `force_collapsing_supply` to true on the mint policy. - -### Create Collection Parameters - -- Max Token Count: The maximum number of unique tokens that can exist within the collection, regardless of the supply of each token. Setting this to `None` allows for the creation of unlimited number of unique tokens within the collection. -- Max Token Supply: The maximum supply limit for each individual token within the collection. Setting this to 1 will ensure each token within the collection is an NFT. -- Force Collapsing Supply: Whether all tokens within the collection should have a collapsing supply. Read more on the available supply types in the [#Create Token Parameters](#create-token-parameters) section below. -- Market Policy: Whether each token within the collection should have forced marketplace royalties. Read more in the [#Create Token Parameters](#create-token-parameters) section below. -- Explicit Royalty Currencies: List of tokens that will be allowed to be used as a royalty currency for each filled listing of a token withing the collection. If no tokens are provided, all tokens will be allowed to be used as a royalty currency. - Read more in the [#Create Token Parameters](#create-token-parameters) section below. -- Attributes: The collection's attributes such as `name`, `description`, `media`, etc. These should be structured according to the [metadata standard](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md). - -When the collection is created, it will emit a `CollectionCreated` event. This event contains the collection id that is used to access the collection. - -![](/img/components/enjin-matrixchain/8.png) - -## How to create a Token - -:::info Some deposits are required. -A single of 0.01 ENJ is required to store the token on-chain. This deposit also covers the first . -An additional of 0.01 ENJ is required for each new token holder. -::: - -Tokens are created using the `mint` extrinsic. It only contains three parameters, the `recipient`, the `collection_id` and `mint params`. The mint params is an enum with a two variants: CreateToken and Mint. - -`CreateToken` is used to create a new token and set its configuration such as cap, metadata, royalty, etc. -`Mint` is used to mint additional units to an existing token. - -### CreateToken - -This must be used the first time a token is created. The provided token id must not already exist. Some additional settings can be chosen when creating a token, such as setting a cap on the supply or giving it a royalty for the marketplace. -Some of these settings can be changed later by using the `mutate_token` extrinsic. - -To create an NFT, set the cap: `supply`/`collapsing supply` to `1`. - -#### Create Token Parameters - -- : A unique token identifier for the new . View the [TokenID Structure](/02-guides/01-platform/03-advanced-mechanics/01-tokenid-structure.md) page for more information. -- Initial Supply: The amount of token supply to initially to the recipient account. -- Account Deposit Count: The number of accounts to reserve ENJ to, for required for future `mint`/`transfer` operations. More info in the [#Account Deposit](#token-account-deposit) section below. -- Supply Cap - - None: Infinite Supply - - Supply: Set a maximum supply amount for the token. Burned units may be re-minted. - - Collapsing: Set a maximum supply amount for the token. Burned units may not be re-minted. (i.e. burning units decreases the maximum supply) -- Token Market Behavior: (can be changed later on using the `MutateToken` extrinsic) - - None: No Royalties. - - Has Royalty: Set a percentage of ENJ royalty for each filled listing on the marketplace. - - isCurrency: Allows this new token to be used as royalty (instead of ENJ royalty) for other tokens in the collection. - **\*Note**: setting a MultiToken currency as royalty isn't implemented on-chain as of now and only acts as a placeholder. -- Listing Forbidden: Whether this token should be prevented from listing on the marketplace (can be changed later on using the `MutateToken` extrinsic) -- Freeze State: - - None: Not initially frozen. - - Permanent: Token will be frozen permanently and can never be transferred to another account. - - Temporary: Token will be frozen temporarily, can be thawed by the collection owner to allow transferring. - - Never: Token will always be transferrable and can never be frozen. -- Metadata: The parameters below are for creating a token with decimal support, like a in-game currency. - - Name: The token name. Can be set to `0x` to provide an empty name. (this name takes precedence when the token name is provided in different parameters such as `uri`/`name` attributes) - - Symbol: The token symbol to be shown in different apps. - - Decimals: The token's decimals count. - Please note, this parameter does not affect the token's behavior on-chain and is used solely for display purposes in applications. It helps apps determine how to format and present the token's total supply. - e.g. A token with `supply: 175` and `decimals: 2` should be formatted as `1.75`. -- ENJ Infusion: - - Infusion: The amount of ENJ to infuse to each unit. (More info in the [#ENJ Infusion](#enj-infusion) section below) - - Anyone Can Infuse: Whether anyone will be able to add infusion to this token, or only the collection owner. - -![](/img/components/enjin-matrixchain/9.png) - -### Mint - -This is used when minting additional units to an already existing token, as long as the circulating token supply didn't reach its cap. - -![](/img/components/enjin-matrixchain/10.png) - -## Transferring tokens and NFTs - -To perform a transfer, use the `transfer` extrinsic. There are two types of transfers: - -### Simple Transfer - -A simple transfer is when the `origin` of the extrinsic is also the sender. i.e. when the account that calling the extrinsic is also the token holder. - -![](/img/components/enjin-matrixchain/11.png) - -### Operator Transfer - -An operator transfer is when an account makes transfers on behalf of another account. This is also known as `transfer_from`. - -![](/img/components/enjin-matrixchain/12.png) - -In the example above, Alice is creating a transaction call to transfer a MultiToken to Bob's account from Charlie's account. -For the transaction to be successful, Bob must approve Alice to transfer this token in advance. - -#### Transfer Approvals - -Approvals can be set for entire collections, or they can be set for specific tokens. They can have expiration times, and specific balances can be set for token approvals. The following extrinsics are used for approvals: - -- **approve_collection -** Approves all tokens in a collection. -- **approve_token -** Approves a specific token in a collection for a specific amount. For security reasons, you must specify the exact amount of the previous approval (or zero if there is none) in the `current_amount` parameter for the extrinsic to succeed. -- **unapprove_collection -** Revokes a collection approval. -- **unapprove_token -** Revokes a token approval. - -## Burning Tokens - -Burning a refers to the act of destroying token units. Tokens can be burned by invoking the `burn` extrinsic. - -For tokens with the "Collapsing Supply" cap type, burning token units decreases the maximum supply of the token, ensuring that any burned units cannot be re-minted. - -### Melting a Token - -"Melting" is a term used to describe the process of burning a that contains an . This process not only destroys the token but also releases the infused ENJ to the token holder. -Read more in the [#ENJ Infusion](#enj-infusion) section below. -Note that "melting" is a conceptual term and does not exist as a specific function in the blockchain code. - -### Destroying Token Account - -When an account burns all of the units it owns from a specific token, its is destroyed and its is released. Read more in the [#Token Account Deposit](#token-account-deposit) section below. - -### Burning and Removing the Token from Storage - -There is an additional field on the `BurnParam` called `remove_token_storage`. If this is set to `true` and the token's circulating supply is zero, the token will be removed from the blockchain storage, effectively destroying the token from existence. This action can only be performed by the collection owner. - -When a is removed from storage, the is returned to the collection owner, and it will be as if the token never existed, so it can be recreated in the future with different configuration. - -![](/img/components/enjin-matrixchain/13.png) - -## Setting/Removing Attributes - -To add or update metadata for a token or a collection, use the `set_attribute` extrinsic. Providing `token_id` sets the attribute to the token, otherwise it sets it directly to the collection. It's only callable by the collection's owner. - -To remove an `attribute`, use `remove_attribute` extrinsic. It's only callable by the collection owner. If the `token_id` is provided, the attribute will be removed from the token. Otherwise, it will be removed from the collection. - -### Setting Collection Attributes - -This is done by clicking the `Manage Attributes` button under the expanded collection. Use the toggle to switch between setting and removing attributes. - -![](/img/components/enjin-matrixchain/14.webp) - -### Setting Token Attributes - -This is done by clicking the `Manage Attributes` button on the token page. Use the toggle to switch between setting and removing attributes. - -![](/img/components/enjin-matrixchain/15.webp) - -## Freezing - -Accounts, collections, and tokens can be frozen to prevent transfers. This is done using the `freeze` extrinsic, which expects freezing `info` to be provided. The `info` specifies whether the , , Collection Account or should be frozen. - -### Freeze a collection or a collection account - -This is done by clicking on Freeze button under expanded collection. -Use the toggle to switch between freezing a specific collection account and the whole collection. - -![](/img/components/enjin-matrixchain/16.png) - -### Freeze a token or a token account - -This is done by clicking on Freeze button on the token page. -Use the toggle to switch between freezing a specific token account and the whole token. - -![](/img/components/enjin-matrixchain/17.png) - -## Thawing - -To unfreeze either collection, token, collection account or token account, use the thaw extrinsic. It expects the same `info` as the `freeze` extrinsic. - -### Thaw a collection or a collection account - -This is done by clicking on the `Thaw` button under expanded collection. -Use the toggle to switch between thawing a specific collection account and the whole collection. - -![](/img/components/enjin-matrixchain/18.png) - -### Thaw a token or a token account - -This is done by clicking on the `Thaw` button on the token page. -Use the toggle to switch between thawing a specific token account and the whole token. - -![](/img/components/enjin-matrixchain/19.png) - -## Batch operations - -It is also possible to perform certain operations in batch. The following operations are supported: - -### Batch Transfer - -Using `batch_transfer` you can batch [transfer](#transferring-tokens-and-nfts) operations, allowing to transfer multiple tokens from a single collection, to a list of `recipients` with different `amount` of tokens. - -![](/img/components/enjin-matrixchain/20.png) - -### Batch Mint - -Using `batch_mint` you can batch [mint](#how-to-create-a-token) operations, allowing minting new or existing tokens, each with it's own `recipient` and `amount`. - -![](/img/components/enjin-matrixchain/21.png) - -### Batch Set Attribute - -Using `batch_set_attribute` you can batch set attribute operations, allowing to set multiple attributes to a single collection / token. If `token_id` is `None`, the attribute is set on the collection. If it is `Some`, the attribute is set on the token. - -![](/img/components/enjin-matrixchain/22.png) - -### Remove All Attributes - -Removes all attributes from the given `collection_id` or `token_id`. -If `token_id` is `None`, it removes all attributes of the collection. If `token_id` is `Some`, it removes all attributes of the token. `attributeCount` must match the number of attributes set in the collection/token, or the transaction will fail. - -![](/img/components/enjin-matrixchain/23.png) - -## ENJ Infusion - -The ENJ Infusion of a token represents the backing value of each unit in ENJ, which is returned to the token holder when it's ed. - -Each token unit may have some ENJ infused into it. The token creator can choose to infuse ENJ to any of its tokens at any given time, or at the time of creation. - -![](/img/components/enjin-matrixchain/24.png) - -Infused ENJ can only be retrieved by [burning the token supply](#burning-tokens). -Burning the token releases the ENJ to the holder. - -In addition, the token creator can choose to allow anyone to add ENJ infusion to an existing token, or restrict it so only the creator can add ENJ infusion. - -![](/img/components/enjin-matrixchain/25.png) - -## Token Account Deposit - -On top of the required to store the token on-chain, a Storage Deposit of 0.01 ENJ is required to store each Token Account on-chain. -A Token Account is created when an account has a balance of 1 or more, and it is destroyed when that balance becomes zero. - -It's important to note that the storage deposit is required for each new Token Account created, not for the total supply of tokens minted. -For example: - -- Minting/transferring 1,000 of a single MultiToken to **a single recipient account that doesn’t already hold a balance of that token** will require creating a Token Account for that recipient. This process incurs a one-time storage deposit of only 0.01 ENJ, covering the entirety of the 1,000 tokens. - -- Minting/transferring any balance of a single MultiToken to **1,000 different recipient accounts, each of which doesn’t already hold a balance of that token**, will require creating a Token Account for each recipient. This process incurs a storage deposit of 0.01 ENJ per account, resulting in a total deposit of 10 ENJ. - -The token creator can reserve some ENJ for Account Deposits on token creation with the `account_deposit_count` field. -ENJ reserve for Account Deposits can be added / removed ahead of time by any account, with the `update_account_deposit` extrinsic. - -On a `mint`/`transfer` operation, if a new Account Deposit is needed and there's no account deposit in reserve, the ENJ required for the account deposit will be taken on demand from the account that performed the mint or transfer. If it is an operator transfer, it is taken from the source account. -In the future, if the created Token Account is destroyed, the ENJ used for the Account Deposit is returned to the account that originally reserved the on-demand deposit. diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/02-fuel-tank-pallet.md b/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/02-fuel-tank-pallet.md deleted file mode 100644 index 220ae21..0000000 --- a/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/02-fuel-tank-pallet.md +++ /dev/null @@ -1,370 +0,0 @@ ---- -title: "Fuel Tanks Pallet" -slug: "fuel-tank-pallet" -description: "Subsidize fees for your players while reducing overall costs." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; - -:::info The Enjin Blockchain Console -Use [console.enjin.io](https://console.enjin.io/) to use the user interface referenced in this document. -You may also check the [hands-on guide for creating and using fuel tanks](/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md) -::: - -## What is a Fuel Tank? - -Fuel Tanks are special accounts that are used purely for transaction fees. Developers can choose to subsidize costs for their customers by depositing ENJ tokens to a Fuel Tank they control. ENJ tokens deposited to a Fuel Tank cannot be withdrawn, but they are released to the tank owner upon tank destruction. - -A fuel tank can whitelist specific tokens, tags, transaction types or users that will be permitted to use it. - -The fuel tank’s ID may be specified in any transaction. The chosen fuel tank will cover transaction costs if its requirements are met. Any remaining costs will be paid by the Fee Payer. - -‍Rules are added to a fuel tank to fine-tune its permissions for specific operators, tokens, time limits and value limits. - -## Terminology - -### Dispatch - -Dispatch refers to the act of broadcasting a transaction via a Fuel Tank, which will subsidize the call's . Depending on the configuration of the Fuel Tank, it may also subsidize any required by the call. - -### Accounts - -#### Tank User Account - -Some fuel tanks require users to register an account to use them. The owner of the fuel tank can specify whether only they can add accounts or if users are allowed to create their own accounts, with or without specific requirements, with the `User Account Management` field. -Each fuel tank account requires a 0.01 ENJ deposit for the Tank User Account - -#### User Account Management - -By default, only the Fuel Tank owner has the permission to add accounts to the Fuel Tank User Accounts. However, the Fuel Tank can be configured to allow accounts to add themselves by setting the `userAccountManagement.tankReservesAccountCreationDeposit` field. - -- **Default Behavior**: If the `userAccountManagement` field is not configured, only the Fuel Tank owner can add accounts to the Fuel Tank User Accounts. -- **Self-Addition without Deposit Funding**: If the `userAccountManagement.tankReservesAccountCreationDeposit` field is set to `false`, the Fuel Tank allows accounts to add themselves to the Fuel Tank User Accounts. However, the Fuel Tank will not provide the funds required for the Tank User Account . -- **Self-Addition with Deposit Funding**: If the `userAccountManagement.tankReservesAccountCreationDeposit` field is set to `true`, the Fuel Tank allows accounts to add themselves to the Fuel Tank User Accounts, and it also covers the necessary funds required for the Tank User Account . - -### Rules - -#### Account Rules - -Account Rules are rules that are validated at the time of Tank User Account Creation. - -#### Dispatch Rules - -Dispatch Rules are rules that are validated at the time of dispatch. -When a dispatch call is made to a fuel tank, it must be made in accordance with a set of rules. These rules, known as rule sets, can include multiple individual rules that determine the validity of the call. A fuel tank can have multiple rule sets, each of which controls access and permissions to the fuel tank's functionality and resources. - -#### Available Rules - -##### Whitelisted Callers: - -Whitelisted callers are accounts that are explicitly granted permission to make calls to a fuel tank and/or create Tank User Accounts. Any calls made by accounts not on the whitelist will be rejected and fail. - -##### Whitelisted Collections: - -Whitelisted collections refer to a list of specific collections allowed to be called by dispatch on a fuel tank. If a dispatch call involves a collection that is not within the whitelisted collections, it will be rejected and fail. This ensures that calls are only made to the specific collections authorized to be accessed. - -##### Max Fuel Burn Per Transaction: - -It is a setting that limits the amount of fuel that can be consumed in a single transaction. It is a safety measure that helps prevent misuse and overuse of the fuel tank's resources by ensuring that a single transaction doesn't consume too much fuel, which is important to ensure a sustainable network. - -##### User Fuel Budget: - -A user fuel budget is the total amount of fuel allocated to a specific user. It sets a limit on the amount of fuel that can be consumed by a user's transactions. Once the user's fuel budget is exhausted, their transactions will fail until the reset period is passed / their account data is removed / more fuel is added to the budget. This is meant to avoid the overuse of resources and ensure a sustainable network. - -##### Tank Fuel Budget: - -A tank fuel budget is the total amount of fuel allocated to a specific rule set. It sets a limit on the amount of fuel that can be consumed by all transactions made through that rule set. Once the tank's fuel budget is exhausted, all transactions will fail until the reset period is passed / more fuel is added to the tank's budget or overall consumption is lowered. - -##### Require Token: - -The "Require token" setting determines that a specific token must be held by the caller for the call to be accepted by the fuel tank. If the caller does not possess the required token, the call will be rejected and fail. This feature is intended to ensure that only users who hold the specified token can access the fuel tank's functions and resources. - -##### Require Signature: - -The "Require Signature" setting determines that a call must be signed by a specific account, and this signature must be included in the dispatch settings for the call to be accepted by the fuel tank. -If the signature is not provided, is invalid or is signed by a different account, the call will be rejected and fail. This feature is particularly useful with [managed wallets](/02-guides/01-platform/02-managing-users/03-using-managed-wallets.md) to allow multiple controlled accounts to use the fuel tank without paying s in advance. - -##### Whitelisted Pallets: - -Whitelisted Pallets refer to a list of specific pallet names that are allowed to be executed using this rule set. This setting only considers the pallet name, not the extrinsics or parameters passed to it. Any call that includes pallets not on the list will be rejected and fail. This ensures that only certain pallets are authorized for calls made by the users, providing a finer level of control over what actions are allowed. - -##### Permitted Extrinsics: - -Permitted extrinsics refer to a list of specific extrinsic function names that are allowed to be executed using this rule set. This setting only considers the extrinsic name, not the parameters passed to it. Any call that includes extrinsics not on the list will be rejected and fail. This ensures that only certain extrinsics are authorized for calls made by the users, providing a finer level of control over what actions are allowed. - -##### Permitted Calls: - -Permitted calls refer to a list of specific extrinsic calls that are allowed to be made using this rule set, including both the extrinsic name and the specific parameters. Any calls made to other extrinsics, or extrinsic calls with non-permitted arguments, will be rejected and fail. This ensures that only authorized extrinsic calls with specific arguments can be made by the users, providing a finer level of control over what actions are allowed. - -### General Tank Terms - -#### Coverage Policy: - -This determines the scope of the fuel tank's subsidy. -Setting the Coverage Policy to "FEES" means it will subsidize only . -Alternatively, setting it to "FEES_AND_DEPOSIT" ensures it covers both and any the dispatched call may require. - -#### Freezing: - -"Freezing" is a state where a fuel tank or a rule set is temporarily prevented from accepting calls. This means that while a fuel tank or rule set is frozen, no dispatches are allowed to occur on it. This is implemented as a safety measure to prevent accidental or malicious changes and to ensure that the fuel tank or rule set remains in a stable state until the freeze is lifted. -Also note that in order to mutate any fuel tank or rule set configurations, it must be frozen first. - -#### Descriptor: - -A descriptor is a list of all the data needed to create a fuel tank. It includes details such as the fuel tank's name, account management rules, rule sets and other configuration information related to the fuel tank. The descriptor acts as a blueprint that defines how the fuel tank is set up and how it will function. It contains all the information needed to create and configure the fuel tank, and can be used to modify the fuel tank's settings. - -#### Require Account: - -The "Require Account" setting determines whether the caller must have a tank account in order to dispatch. -If "Require Account" is set to true, the caller must have a tank account; otherwise, their dispatch will be rejected and fail. -If "Require Account" is set to false, the caller can dispatch even without a tank account. -It is important to note that when "Require Account" is set to false, anyone will be able to dispatch, which could quickly drain the tank funds. Therefore, it is crucial to implement strict dispatch rules when using "Require Account: false" to prevent misuse. - -### Extrinsics - -#### create_fuel_tank: - -Create a new fuel tank by providing a descriptor. Extrinsic generates a discrete `AccountId` for the fuel tank based on passed in parameters, it takes a storage deposit and emits `FuelTankCreated` event in the success case. Creation of already existing fuel tank will result in `FuelTankAlreadyExists` error, while duplicate rule kinds within a rule set will result in `DuplicateRuleKinds` - -#### mutate_fuel_tank: - -Applies provided mutation to the fuel tank. Caller must be the owner of the fuel tank, otherwise call will result in `NoPermission` error. Tank must be frozen in order to mutate it, otherwise call will result in `RequiresFrozenTankOrRuleset` error. Duplicate rule kinds within a rule set will result in `DuplicateRuleKinds`. In success case emits `FuelTankMutated` event - -#### add_account: - -Adds a new fuel tank user account, which not only allows using fuel tank, but also stores user consumption and rule set data. An account can be created only if account rules are successfully validated. An account is required to dispatch calls when the ruleset's `require_account` is set to `true`. -A storage deposit is required, and may be paid by the user or the fuel tank, depending on the settings. Could fail with `NoPermission` if caller is not the owner and user management settings don't allow users to create accounts on their own. Creation of already existing account withing a tank will result in `AccountAlreadyExists` error. In case some of account rules cannot validate the caller, rule specific error will be returned. In success case emits `AccountAdded` event - -#### remove_account: - -Removes a user account from a fuel tank and returns the storage deposits. Fuel tank must be frozen for operation to succeed, otherwise call will result in `RequiresFrozenTank` error. Account must not contain any rule data, otherwise call will result in `AccountContainsRuleData` error. In success case emits `AccountRemoved` event - -#### remove_account_rule_data: - -Remove account rule data if it exists. Requires the fuel tank or the rule set to be frozen, otherwise will result in `RequiresFrozenTankOrRuleset` error. Only callable by the fuel tank's owner, otherwise will result in `NoPermission` error. In success case emits `AccountRuleDataRemoved` event - -#### dispatch: - -Dispatch a call through the fuel tank, where the tank would pay for transaction fees and, if configured, provide a storage deposit. -Additional settings can be provided with the `Settings` argument such as `provide remaining fee`, or include a signature required for the `RequireSignature` dispatch rule. -All calls are subject to rule set evaluation and would result in rule specific errors in case of failure. In case the inner call fails, the `DispatchFailed` event will be emitted with wrapped dispatch error inside. In success case emits `Dispatched` event. - -#### dispatch_and_touch: - -Same as dispatch, but also does create an account for the caller with `add_account` operation if it doesn't exist. Fuel tank's `user_account_management` settings must allow self-service account creation, otherwise call will result in `NoPermission` error. - -#### schedule_mutate_freeze_state: - -Schedule mutating of fuel tank or rule set `is_frozen` state for `on_finalize` to execute with help of `process_freeze_queue` helper function. Additional 1 read and 1 write are added to account for `on_finalize` storage operations. Only callable by the fuel tank's owner, otherwise will result in `NoPermission` error. In case queue already has maximum number of items, will result in `FreezeQueueFull` error. In success case emits `MutateFreezeStateScheduled` event - -#### insert_rule_set: - -Insert a new rule set into a fuel tank. It can be a new rule set or it can replace an existing one. If it is replacing a rule set, a rule that is storing data on any accounts cannot be removed and will result in `CannotRemoveRuleThatIsStoringAccountData` error. Use `remove_account_rule_data` to remove the data first. If a rule is being replaced, it will be mutated with the new parameters, and it will maintain any persistent data it already has. Fuel tank or rule set must be frozen, otherwise will result in `RequiresFrozenTankOrRuleset` error. If fuel tank already has maximum number of rule sets, will result in `MaxRuleSetsExceeded` error. Duplicate rule kinds within a rule set will result in `DuplicateRuleKinds`. In success case emits `RuleSetInserted` event - -#### remove_rule_set: - -Remove rule set from a fuel tank. A rule that is storing data on any accounts cannot be removed and will result in `CannotRemoveRuleThatIsStoringAccountData` error. Use `remove_account_rule_data` to remove the data first. This is only callable by the fuel tank's owner, otherwise will result in `NoPermission` error. Fuel tank or rule set must be frozen, otherwise will result in `RequiresFrozenTankOrRuleset` error. In success case emits `RuleSetRemoved` event - -#### batch_add_account: - -Same as `add_account` but takes a list of `AccountIds` to add into a fuel tank - -#### batch_remove_account: - -Same as `remove_account` but takes a list of `AccountIds` to remove from a fuel tank - -#### force_set_consumption: - -Force set the fuel tank consumption. If `user_id` is `Some`, it sets the consumption for that account. If it is `None`, it sets the consumption on the fuel tank directly. Currently only callable by fuel tank owner and sudo account, otherwise will result in `NoPermission` error - -#### destroy_fuel_tank: - -Destroy the fuel tank. Only callable by owner, otherwise will result in `NoPermission` error. The fuel tank must be frozen, otherwise will result in `DestroyUnfrozenTank` error. Can only be destroyed if all accounts are removed, otherwise will result in `DestroyWithExistingAccounts` error. Returns the storage deposit and remaining balance to the fuel tank owner. In success case emits `FuelTankDestroyed` event - -## How to create a fuel tank? - -A fuel tank can be created by anyone using the `create_fuel_tank` extrinsic function, once the fuel tank is created, the owner can add funds to it and subsidize transactions that are permitted by the fuel tank. The primary function of a fuel tank is to allow a user to interact with Enjin Matrixchain without paying transaction fees (called gas fees in Ethereum). - -Create a new fuel tank by providing a `descriptor`. The extrinsic generates a discrete `AccountId` for the fuel tank based on passed in parameters, it takes a storage deposit and emits `FuelTankCreated` event in the success case. Creation of already existing fuel tank will result in `FuelTankAlreadyExists` error, while duplicate rule kinds within a rule set will result in `DuplicateRuleKinds`. - -To create a new fuel tank using the [explorer](https://console.enjin.io/#/explorer), head to `Network` → `Fuel tanks` → `Create Fuel Tank`. This page allows you to create a new fuel tank by specifying the following options: - -- **Name:** Name for the fuel tank. -- **[User Account Management](#user-account-management):** (Optional) Allows setting the permission level to add new accounts to the fuel tank. -- **[Coverage Policy](#coverage-policy):** \ → Specify the scope of the fuel tank's subsidy. -- **[Account Rules](#account-rules):** (Optional) Allows specifying rules that are evaluated before an account is added to the fuel tank. -- **[Dispatch Rules](#dispatch-rules):** (Optional) Allows specifying rules that are evaluated before an extrinsic is dispatched via the fuel tank. - -:::warning -Even though the account rules and dispatch rules are optional, it is recommended to have some rules setup for your fuel tank, a fuel tank without any rules might be open to anyone without limits. -::: - -This image shows how to create a fuel tank, with some basic rules, in the following fuel tank configuration Alice creates a fuel tank that provides storage deposits for operations that require it. The tank consists of a single ruleset that doesn't require a Tank User Account to dispatch calls, and only Bob and Charlie can dispatch calls via this ruleset. - -![](/img/components/enjin-matrixchain/26.png) - -## How to modify a fuel tank? - -To mutate or modify a fuel tank, the caller must be the owner of the fuel tank, otherwise call will result in `NoPermission` error. Tank must be frozen in order to mutate it, otherwise call will result in `RequiresFrozenTankOrRuleset` error. Duplicate rule kinds within a rule set will result in `DuplicateRuleKinds`. In success case emits `FuelTankMutated` event - -To mutate a fuel tank using the explorer, head to `Network` → `Fuel tanks`. Mutating a fuel tank involves three steps 1. Freeze the fuel tank 2. Mutate the fuel tank 3. Unfreeze the fuel tank - -1. To Freeze the fuel tank, head to `Network` → `Fuel tanks`, this will present a list of all fuel tanks, select the fuel tank you wish to mutate and select freeze/unfreeze. In the next dialog box, set `is_frozen` to `true` and execute the transaction -2. To mutate the fuel tank, head to `Network` → `Fuel Tanks` → under specific fuel tank → `Mutate`In the modal, Select the Account (the owner of the fuel tank) and select the fuel tank id (you can get this from the fuel tanks page in the previous step) and add any modifications to the fuel tank. - If successful, you should see the event with you changes in the explorer `Network` tab -3. To unfreeze the fuel tank, head to `Network` → `Fuel tanks`, and perform the same step but select the `is_frozen` to `false` - -## Add an account to a fuel tank - -Adds a new fuel tank user account. An account can be created only if [account rules](#account-rules) are successfully validated. An account may be required to dispatch calls. A storage deposit is required, and may be paid by the user or the fuel tank, depending on the settings. Could fail with `NoPermission` if caller is not the owner and user management settings don't allow users to create accounts on their own. Creation of already existing account withing a tank will result in `AccountAlreadyExists` error. In case some of account rules cannot validate the caller, rule specific error will be returned. In success case emits `AccountAdded` event - -To add an account to a fuel tank using the explorer, head to `Network` → `Fuel tanks`: - -- Select the fuel tank and click add account -- Select account to be added. -- On success you should see an `AccountAdded` event - -## Remove an account from fuel tank - -Removes a user account from a fuel tank and returns the storage deposits. Fuel tank must be frozen for operation to succeed, otherwise call will result in `RequiresFrozenTank` error. Account must not contain any rule data, otherwise call will result in `AccountContainsRuleData` error. In success case emits `AccountRemoved` event - -To remove an account from a fuel tank using the explorer: - -1. Head to `Network` → `Fuel tanks`, and follow similar instructions to remove an account. -2. Select the fuel tank and click remove account - -![](/img/components/enjin-matrixchain/27.png) - -## Remove an account rule data - -As part of enforcing the budget and max spending limits of the fuel tank, the fuel tank stores data for every account. The `remove_account_rule_data` allows to remove a user account rule data if it exists. Requires the fuel tank or the rule set to be frozen, otherwise will result in `RequiresFrozenTankOrRuleset` error. Only callable by the fuel tank's owner, otherwise will result in `NoPermission` error. In success case emits `AccountRuleDataRemoved` event. - -:::warning -Similar to the process of mutating a Fuel Tank, the Fuel Tank must be frozen for the operation to succeed, otherwise the call will result in an error. -::: - -To remove an account rule data, head to `Developer` → `Extrinsic` → `fuelTanks` → `removeAccountRuleData`, and select the `tankId`, `accountId` to remove data from and the rule set and rule type to remove data from. - -![](/img/components/enjin-matrixchain/28.png) - -## Dispatch a transaction using a fuel tank - -Dispatch a call through the fuel tank, where the tank would pay for transaction fees and, if configured, provide a storage deposit. All calls are subject to rule set evaluation and would result in rule specific errors in case of failure. In case the inner call fails, the `DispatchFailed` event will be emitted with wrapped dispatch error inside. In success case emits `Dispatched` event. - -To dispatch a transaction using a fuel tank, head to Network → Fuel tanks, select the fuel tank you wish to use and select Dispatch. - -Inside the dialog box that opens, select: - -- The extrinsic you want to call via the fuel tank. -- The rule set id you wish to apply to the transaction. -- The extrinsic to execute. -- The dispatch settings if necessary. - -![](/img/components/enjin-matrixchain/29.png) - -On success, you should see the `CallDispatched` event. - -## Selecting a fuel tank to dispatch with - -The `fuelTanks_selectFuelTank` API method is a part of the matrix runtime, designed to allow users to select a fuel tank from a list of given fuel tanks. Here are step-by-step instructions on how to call this method, including the necessary parameters and expected output. - -### Method Definition - -```rust -#[method(name = "fuelTanks_selectFuelTank")] -fn select_fuel_tank( - &self, - caller: Address, - call: Vec, - settings: Vec, - fuel_tanks: Vec
, - at: Option, -) -> RpcResult>; -``` - -### Parameters - -- `[Required]` caller (Address): The address of the caller who is invoking the method. -- `[Required]` call (`Vec`): A vector of bytes representing the call that is to be made. -- `[Required]` settings (`Vec`): A vector of bytes representing the settings to be applied during the call. This should match the settings provided during the actual call or the selction will not work correctly. -- `[Required]` fuel_tanks (`Vec
`): A vector of addresses representing the available fuel tanks to select from. If no fuel tanks are provided then all fuel tanks are searched. -- at (Option\>`, which can be interpreted as: - -- `Option<(Address, u32)>` : An optional tuple where: - `Address`: The selected fuel tank's address. - `u32`: The amount of fuel (funds) available in the selected tank. - -If no appropriate fuel tanks are found the rpc will return null. - -### How it works - -The provided RPC method `fuelTanks_selectFuelTank` works by internally selecting the most suitable fuel tank for a transaction based on the caller's request and the set of predefined rules. The evaluation process considers various factors such as the fee required to execute the transaction, whether the fuel tank is frozen, and if the caller's account is associated with the fuel tank. The goal is to find the fuel tank that can cover the transaction costs with the least expense to the user while complying with all applicable rules. - -It is important to note that this RPC method relies heavily on the proper configuration and the correct availability of fuel tanks. If the settings passed during the selectFuelTank phase and actual call differs then the result might be unreliable. - -### Example - -- PolkadotJS - -```javascript -import { ApiPromise, Keyring, WsProvider } from '@polkadot/api'; - -const MATRIX_URL = "ws://127.0.0.1:56287"; - -async function sendTransaction() { - // Initialize the provider - const provider = new WsProvider(MATRIX_URL); - - // Create the API instance - const api = await ApiPromise.create({ - provider, - rpc: { - fuelTanks: { - selectFuelTank: { - params: [ - { name: 'callerAddress', type: 'AccountId' }, - { name: 'call', type: 'Vec' }, - { name: 'settings', type: 'Vec' }, - { name: 'fuelTanks', type: 'Vec' }, - { name: 'at', type: 'Hash', isOptional: true } - ], - }, - } - } - }); - - // Create a keyring instance and add Alice's account - const keyring = new Keyring({ type: 'sr25519' }); - const alice = keyring.addFromUri('//Alice'); - - // Define recipient and amount for the transfer - const recipient = 'cxMefrbkBrS5CQNTsZNxrntTMnakzVLnR9d5f3q5TuhDBynex'; - const amount = 1000000000000; - - // Create the transfer call - const transferCall = api.tx.balances.transferKeepAlive(recipient, amount); - - // Debug: Log the transfer call - console.log('Transfer call (hex):', transferCall.toHex()); - - // Define settings and fuelTanks with proper types - const settings = new Uint8Array(); // or the actual settings if available - const fuelTanks = []; // Pass an empty array if no specific fuel tanks are provided - - // Perform the RPC call to select the fuel tank - const response = await api.rpc.fuelTanks.selectFuelTank( - "cxMefrbkBrS5CQNTsZNxrntTMnakzVLnR9d5f3q5TuhDBynex", - transferCall.tou8a(), - settings, - fuelTanks, - null // Use 'null' for optional 'at' parameter if not required - ); - - // Debug: Log the response - console.log('Response:', response); -} -``` diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/03-enjin-matrixchain.md b/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/03-enjin-matrixchain.md deleted file mode 100644 index ea7476d..0000000 --- a/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/03-enjin-matrixchain.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: "Enjin Matrixchain" -slug: "../enjin-matrixchain" -description: "Layer 1: The utility layer of the Enjin Blockchain." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; - -## What is the Enjin Matrixchain? - -Enjin is a blockchain platform that provides a developer-friendly Matrixchain built on the Enjin Relaychain specifically designed for digital assets. Unlike most digital assets that run on general-purpose chains, Enjin provides faster on-chain digital asset creation and interactions, greater security, interoperability, and flexibility. -Enjin's digital asset functionality is built right into the protocol level, allowing developers to focus on building their apps instead of worrying about security and functionality. Enjin's developer-friendly approach offers a simplified and streamlined experience that can accelerate the creation and adoption of digital assets on the blockchain. - -## Enjin Matrixchain Features - -![](/img/components/enjin-matrixchain/30.webp) - -The Enjin Matrixchain incorporates an array of features that enable digital asset creation, minting, transfer, on-chain marketplace actions, royalties, crafting, swapping, fuel tanks, and more. These features are integrated into the chain protocol, making them readily accessible to all users without requiring additional setup. However, if users require specific functionality not included in the core protocol, they can leverage custom smart contracts to extend the capabilities of the platform. This flexibility ensures that Enjin can accommodate a wide range of use cases, from simple transactions to complex asset interactions. - -- Asset minting: Minting assets on Enjin is highly customizable, supporting the creation of both and non-fungible () assets, with capped or uncapped supply. With these features, Enjin provides a versatile platform for creating and distributing digital assets that can meet a wide range of use cases. -- Fuel Tanks: Enjin's Fuel Tanks feature enables the subsidization of transaction fees for different actions on the platform. This feature is highly customizable, allowing users to define custom parameters and rules that suit their specific use case. By providing this flexibility, Enjin can accommodate a wide range of transaction types, from simple transfers to more complex asset interactions. -- On-Chain Marketplace: The On-Chain Marketplace offers marketplace functionality at the protocol level, which allows for the trading of any item without requiring token listings or even their existence on the network. This feature enhances price discovery by enabling bids on non-listed tokens. Additionally, the marketplace functionality on the protocol level guarantees that royalties will be credited for all asset sales, regardless of the marketplace on which the sale occurred, without requiring custom smart contracts. diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/03-marketplace-pallet.md b/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/03-marketplace-pallet.md deleted file mode 100644 index c4d5701..0000000 --- a/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/03-marketplace-pallet.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: "Marketplace Pallet" -slug: "marketplace-pallet" -description: "Create secure peer-to-peer economies." ---- -:::info The Enjin Blockchain Console -Use [console.enjin.io](https://console.enjin.io/) to use the user interface referenced in this document. -::: - -## Overview - -The marketplace is used for trading assets on `pallet-multi-tokens`. - -## Terminology - -### Listing - -Represents an asset being put up for sale. It contains additional information that determines the behavior of the sale. - -### Auction - -A listing with an end time that accepts bids. - -### Offer - -A listing that is a request to buy an asset. - -### Royalty - -A percentage of a sale is given to an account when an asset is sold. - -### Salt - -Bytes used to differentiate listings that have the same values. - -### Make Asset - -The asset being sold. -When creating an offer, it's the asset being requested. - -### Take Asset - -The asset requested. -When creating an offer, it's the asset being bought. - -### Fee side - -The side that is charged the protocol fee (the make or take side). - -### Currency - -This concept is used by the marketplace to determine what assets are allowed to be listed, and which side of the listing is charged fees. - -## Config Values - -- `maxRoundingError: Balance = 1000` - - The max amount that can be lost due to rounding before failing. -- `maxSaltLength: u32 = 32` - - Max length of salt used in listings and bids -- `minimumBidIncreasePercentage: Perbill = 5%` - - The percentage of the minimum bid in an auction must increase by. -- `listingActiveDelay: BlockNumber = 5` - - The minimum number of blocks that must pass before a listing is active. - -## Types - -- `ListingId = H256 ([u8;32])` -- AssetId - a `CollectionId` and `TokenId` - -See pallet-multi-tokens for additional types, including `CollectionId`, `TokenId`, and `Balance`. - -## Listings - -Listings are created with the `create_listing` extrinsic. There are three types of listings: - -- Fixed Price listings allow assets to be purchased for a specific price and have no time limit. Assets are purchased using the `fill_lisitng` extrinsic. Partial fills are supported. -- Timed Auctions operate like an auction. The listing is created with an initial bid price and a time limit. Bids are placed with the `place_bid` extrinsic and must start at the initial price or more, and then each bid must increase by a fixed percentage (currently 5%). When the time limit is over, the `finalize_auction` must be called to officially end the auction and transfer the funds. -- Offer listings are created by the buyer. Seller may accept the offer using the `fill_lisitng` extrinsic. The seller and the buyer may counter offer using the `placeCounterOffer` extrinsic, in which case they can accept / reject / counter offer using the `AnswerCounterOffer` extrinsic. - -## Protocol Fee - -A protocol fee (currently 2.5%) is taken from all sales. - -## Royalties - -Royalties are currently limited to between 0.1% and 50%. If a royalty is changed after an asset is listed and the seller would get less than they expect, the sale does not go through. This is stored in the `minimum_take_value` field of the listing. - -A collection can only allow certain currencies to be accepted as royalties by adding them to its `explicit_royalty_currencies` field. If a listing is created that will pay royalties in an unaccepted currency, the listing creation will fail. - -## Fee Side - -The fee side determines which side of the listing, the make side or take side, will be charged the protocol fee. The royalty is determined by the opposite side of the fee side, and it is paid in the currency of the fee side. - -The fee side is determined with the following checks: - -1. If the make side is a currency, it pays the fee. -2. If the make side is not a currency, but the take side is a currency, the take side pays the fee. -3. If neither side is a currency, there is no fee. However, this is not currently allowed, and the listing cannot be created. - -## State - -Stores information about the marketplace. - -`listings` - -- Key: `ListingId` - The id of the listing -- Value: Listing -- Stores all listings. - -`listingIdsByMakeAsset` - -- Key: (`CollectionId`, `TokenId`) - The collection id and token id of the make asset. -- Value: `ListingId `- The id of the listing -- Allows querying listings by make asset. - -`listingIdsByTakeAsset` - -- Key: (`CollectionId`, `TokenId`) - The collection id and token id of the take asset. -- Value: `ListingId `- The id of the listing -- Allows querying listings by take asset. - -`listingIdsByAccountId` - -- Key: `AccountId `- The account id of the seller -- Value: `ListingId `- The id of the listing -- Allows querying listings by the seller. - -## Extrinsics - -`create_listing` - Places a sell / auction / offer order. - -The listing does not become active until after the `listingActiveDelay` (5 blocks) has passed. If it's an auction, it starts when its `start_block` is reached. - -A Token cannot be listed on the marketplace if its listing_forbidden field is set to true. - -**`cancel_listing`** - Cancels the listing. Only callable by the seller. - -**`fill_listing`** - Fills a fixed price listing. This will execute immediately. - -**`place_bid`** - Places a bid on a listing. The listing must be an auction, and it must be currently active. An auction is considered active if the current block is between the start and end blocks of the auction. - -Only the latest bid is stored on an auction. All bids must increase by the `minimumBidIncreasePercentage`, and the first bid must be higher than the price set when the listing was created. - -**`finalize_auction`** - This will end the auction and transfer funds. It fails if the auction is not over. It can be called by anyone. - -**`place_counter_offer`** - Creates a Counter offer to an existing offer listing. Can only be called by an account that owns the token. - -**`answer_counter_offer`** - Either accept, reject, or counter a counter offer. Callable by the offer creator (buyer) and the counter offer creator (seller). -When a counter offer is successfully accepted/rejected, it is removed from storage. - -**`set_protocol_fee`** - Change the protocol fee. Can only be called by the `ProtocolFeeOrigin` set in the config. diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/_category_.json b/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/_category_.json deleted file mode 100644 index 2e2a91c..0000000 --- a/versioned_docs/version-2.x/04-enjin-blockchain/03-enjin-matrixchain/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Enjin Matrixchain" -} - \ No newline at end of file diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/04-enjin-blockchain.md b/versioned_docs/version-2.x/04-enjin-blockchain/04-enjin-blockchain.md deleted file mode 100644 index 6a17264..0000000 --- a/versioned_docs/version-2.x/04-enjin-blockchain/04-enjin-blockchain.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: "Enjin Blockchain infrastructure" -slug: "infrastructure" ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; - -## What is Enjin Blockchain? - -The Enjin Blockchain is a Layer 1 protocol designed with native NFT functionalities embedded within the protocol layer. The network is scalable and efficient because it is governed by a delegated Proof-of-Stake () consensus protocol. At the time of writing the Enjin is able to process over 5,000 transactions per second (). - -This documentation provides technical details and guidance for developers aiming to integrate with or leverage the Enjin Blockchain's capabilities. - -Enjin Blockchain is made up of the Enjin Relaychain and the Enjin . - -![](/img/components/blockchain-infrastructure/1.png) - -### Enjin Relaychain - -The Enjin Relaychain serves as the central backbone of the Enjin ecosystem. It acts as a hub where each can their holdings in ENJ and undergo validation processes. The Relaychain primarily focuses on a limited set of transaction types, which encompass various ways to engage with the governance mechanism. Its purpose is deliberately streamlined, purposely omitting support for smart contracts. The primary function of the Relaychain revolves around system coordination, encompassing both itself and the interconnected Matrixchains. Meanwhile, specific tasks and functionalities are delegated to the Matrixchains, each with unique implementations and features. - -In essence, a Matrixchain represents an application-specific data structure that possesses global coherence and can be validated by the validators participating in the Relaychain. These Matrixchains are specifically designed to cater to the needs of different projects, offering specialized functionalities tailored to non-fungible tokens (NFTs). This allows for customization options and isolated data storage, empowering various projects to establish their own distinctive internal economies. - -### Enjin Matrixchain - -The Enjin Matrixchain, on the other hand, stands as a decentralized, permissionless, and trustless network that provides opportunities for anyone to operate a and actively participate in the network's activities. The network's underlying architecture aims to create a scalable and secure platform dedicated to NFTs at the protocol level. Leveraging the Substrate framework, the Enjin Matrixchain endeavors to deliver a robust infrastructure that ensures the seamless functioning and growth of the NFT ecosystem. diff --git a/versioned_docs/version-2.x/04-enjin-blockchain/_category_.json b/versioned_docs/version-2.x/04-enjin-blockchain/_category_.json deleted file mode 100644 index 902721c..0000000 --- a/versioned_docs/version-2.x/04-enjin-blockchain/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Enjin Blockchain" -} - \ No newline at end of file diff --git a/versioned_docs/version-2.x/05-enjin-platform/02-chain-state.md b/versioned_docs/version-2.x/05-enjin-platform/02-chain-state.md deleted file mode 100644 index e7101af..0000000 --- a/versioned_docs/version-2.x/05-enjin-platform/02-chain-state.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -title: "Reading Chain State" -slug: "reading-chain-state" -description: "Learn how to monitor and manage the chain state for your Enjin blockchain platform, ensuring optimal performance and data integrity." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; - -The chain state of a blockchain is a snapshot of all the data it contains at a given moment. - -This includes account balances, ownership of digital assets, and other on-chain information. - -It's regularly updated through the network's consensus algorithm to maintain data integrity. - -To query this state, there are several methods, but let's focus on using the Enjin Console and the Enjin Platform: - -## Enjin Platform - -The Enjin Platform uses a blockchain indexer to gather transactions, aiming to provide the latest chain state as quickly as possible. - -Direct queries to the blockchain via an RPC node are possible but can be demanding on resources, especially with many simultaneous queries. Indexers mitigate this issue. - -With the Enjin Platform, you can use advanced queries involving multiple relationships to get data faster than directly from RPC nodes. - -Here's an example of a query in the GraphiQL playground, showing the state of collection ID 2000: - -```graphql -query GetCollection { - GetCollection(collectionId: 2000) { - owner { - account { - address - } - } - maxTokenCount - maxTokenSupply - forceSingleMint - frozen - tokens { - totalCount - edges { - node { - tokenId - } - } - } - } -} -``` -```json -{ - "data": { - "GetCollection": { - "owner": { - "account": { - "address": "efTpCuJYg7jnjA8HxYb9dFKp7eCP7WKKHVoCjqy1Dt7NABds6" - } - }, - "maxTokenCount": null, - "maxTokenSupply": null, - "forceSingleMint": false, - "frozen": false, - "tokens": { - "totalCount": 0, - "edges": [] - } - } - } -} -``` - -:::warning Current Chain State -It's important to know that the Enjin Platform only keeps track of the current state of the chain. This means you can't check what happened in the past blocks. The platform is designed for giving you up-to-date information about the chain's current state, not for looking at its history. -::: - -## Enjin Console - -### Connecting to the Enjin Console - -Open the Enjin Console, you can connect to the network by clicking on the following URL: https://console.enjin.io/ - -![Enjin Console Chain State](/img/guides/going-open-source/enjin-console-chain-state.png) - -## Querying the data - -To look up data from a specific module (called a ) on the Chainstate page of the Enjin Console, you just need to pick the pallet you're interested in and then select the data item you need from that pallet. - -Remember that the database used by the blockchain node is structured like a tree (specifically, a radix tree), which is different from a standard SQL database. - -- **Pallet Storage** on a blockchain is like a separate database for each category of data. -- **Specific Storage** within a pallet is akin to a table in a database, holding actual data entries. - -Unlike a traditional SQL database with tables, a blockchain organizes data in a tree structure for efficiency, but the concept of separate areas for different data types (pallets) and specific datasets (storages) within those areas is similar. - -![Querying data from the Enjin Console](/img/guides/going-open-source/enjin-console-querying-data.png) - -:::tip Specifying the Block Hash -If you do not enter the block hash to query, the chain state displayed to you will always represent the latest block created in the network. -::: diff --git a/versioned_docs/version-2.x/05-enjin-platform/03-working-with-events.md b/versioned_docs/version-2.x/05-enjin-platform/03-working-with-events.md deleted file mode 100644 index 6b96dbf..0000000 --- a/versioned_docs/version-2.x/05-enjin-platform/03-working-with-events.md +++ /dev/null @@ -1,177 +0,0 @@ ---- -title: "Working with Events" -slug: "working-with-events" -description: "Explore how to handle blockchain events within the Enjin platform, enabling real-time updates and interactions in your application." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In the blockchain ecosystem, "events" refer to important actions or changes within the blockchain network. - -This includes things like creating or changing digital assets, transferring balances, and any other actions that alter the chain's state. These events are recorded on the blockchain, providing a transparent and unchangeable history of all activities. - -Monitoring and analyzing these events helps users and developers understand the flow of transactions and respond to specific actions or conditions in the network. This is key for decentralized applications and games, enabling automated interactions and improving overall functionality. - -:::warning Note on Address Formats in Platform Events -Please note that all account addresses included in platform events, such as channel names or any addresses within the event payload, are formatted as **public keys**. -For example, in a "Transfer" event, the "from" account address appears in its public key format. -If needed, you may use the [Account Format Transform tool](https://matrix.subscan.io/tools/format_transform) to convert between **SS58-encoded address format** (e.g., "cx...123" for the Canary Matrixchain or "cn...123" for the Canary Relaychain) and public key format. -::: - -## Using Enjin Platform - -To view events emitted from a transaction conducted on the platform, you can use the `GetTransaction` query as follows: - - - -```graphql -query GetTransaction { - GetTransaction(id: 40000) { - method - state - result - events { - edges { - node { - moduleld - eventId - params { - type - value - } - } - } - } - } -} -``` - - -```json -{ - "data": { - "GetTransaction": { - "method": "BatchTransfer", - "state": "FINALIZED", - "result": "EXTRINSIC_SUCCESS", - "events": { - "edges": [ - { - "node": { - "moduleId": "Balances", - "eventId": "Withdraw", - "params": [ - { - "type": "who", - "value": "f64fda4cb0352aff9394f0cc14495ca8cefffb8c8e9b37de058a1d4d4650c3313" - }, - { - "type": "amount", - "value": "20675071170417823" - } - ] - } - }, - { - "node": { - "moduleId": "MultiTokens", - "eventId": "CollectionAccountCreated", - "params": [ - { - "type": "collection_id", - "value": "2155" - }, - { - "type": "account", - "value": "303049fa4535f21acc5760521c6cc91f01ddf28ebf771da99effbf5c1031416F" - } - ] - } - } - ] - } - } - } -} -``` - - - -Our platform broadcasts network events through a `WebSocket` channel, similar to the recent events feature in Polkadot-JS. It's important to note that this channel includes not only network-wide events but also specific events from the Enjin Platform. - -Learn more about [Websocket Events here](/05-enjin-platform/03-working-with-events.md). - -![Platform Events](/img/guides/going-open-source/platform-events.png) - -Our platform uses WebSockets to transmit real-time events, but since WebSockets can sometimes be unstable, there's a risk of missing events if your system loses connection. To address this, we also cache events in a query called `GetPendingEvents`. The most effective way to stay updated in real time and ensure no events are missed is to always `acknowledge` the events you receive. - - - -```graphql -query GetPendingEvents { - GetPendingEvents( - acknowledgeEvents: false, - first: 50 - ) { - edges { - node { - name - data - } - } - } -} -``` - - -```json -{ - "data": { - "GetPendingEvents": { - "edges": [ - { - "node": { - "name": "platform:transaction-updated", - "data": { - "id": 179419, - "state": "PROCESSING", - "method": "BatchTransfer", - "result": null, - "transactionId": null, - "idempotencyKey": "4eace652-9c9f-4721-bfe3-1be6f7597c32", - "transactionHash": null - } - } - }, - { - "node": { - "name": "platform:transaction-updated", - "data": { - "id": 179417, - "state": "PROCESSING", - "method": "BatchTransfer", - "result": null, - "transactionId": null, - "idempotencyKey": "a3e62297-983e-4a14-81a9-975976302127", - "transactionHash": null - } - } - } - ] - } - } -} -``` - - - -## Using Enjin Console - -In the explorer tab, you can find the recent events emitted in the network. Those will appear as soon as they are emitted. - -![Viewing events on Enjin Console](/img/guides/going-open-source/enjin-console-events.png) - -You can also see the events emitted in a single block by querying the block directly - -![Viewing events on Enjin Console](/img/guides/going-open-source/enjin-console-events-2.png) diff --git a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/02-local-installation.md b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/02-local-installation.md deleted file mode 100644 index 0612873..0000000 --- a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/02-local-installation.md +++ /dev/null @@ -1,187 +0,0 @@ ---- -title: "Local Installation" -slug: "local-installation" -description: "Learn how to install Enjin's blockchain tools locally on your system, enabling you to develop and test your applications offline or in a controlled environment." ---- -## Running it locally - -The [Enterprise On-Prem Enjin Platform](https://github.com/enjin/platform) was made to be as simple as possible to run, you will be able to have a full running environment in less than 5 minutes. - -### 1\. Install Dependencies - -We have two dependencies that must be installed in your server or computer, git, a command line version control system, you can download it here: https://git-scm.com/downloads - -And docker, you may install either the [docker engine](https://docs.docker.com/engine/install/) or [docker desktop](https://www.docker.com/products/docker-desktop), though for people that have never used docker before we recommend using docker desktop: https://www.docker.com/products/docker-desktop - -### 2\. Clone the Repository - -In order for this to work you have to clone the repository, please note that downloading it as a zip will generate problems with the submodules. - -To clone the repository just run the following command in the Terminal or Command Prompt in the folder where you want to download the platform. - -```bash -git clone https://github.com/enjin/platform -``` - -:::info Logging in -You login by editing the `configs/core/.env file`. Here, you use the `DAEMON_ACCOUNT` and `BASIC_AUTH_TOKEN` environment variables for the wallet account and API token, respectively. -::: - -### 3\. Run the Start Script - -In the main folder of the platform, there are two start scripts: `start.bat` for Windows and `start.sh` for macOS and Linux. - -Running either script sets everything up automatically. - -#### Running the Start script on Windows - -To run the Start script on windows, simply double click the `start.bat` file. - -#### Running the Start script on MacOS / Linux - -To run the Start script on MacOS / Linux, open your Terminal application on MacOS, or establish an SSH connection to your Linux server. -Then, navigate to the platform's root directory using the `cd platform` command, and start the script using the following command: - -```bash -./start.sh -``` - -*** - -A sample output is available to show what happens during this process. - -```text -Welcome to Enjin Platform, this script will help you start it up -For a few things to work, we need to know your platform URL -Please input the URL: (e.g. http://127.0.0.1) -http://127.0.0.1 - -Laravel uses an app key to protect your data with encryption -Your APP_KEY is not set, do you want to generate one? (y/n) -y - -Done, your key is: base64:CGAdVfzKICzydRuipAxt9UxlbfMjggDiiT52T7P9H0A= -We also use a static token to protect your platform from unauthorized access -Your BASIC_AUTH_TOKEN is not set, do you want to generate one? (y/n) -y - -Done, your basic static token is: 01f093ba63b4285b0ec451f406e3d8580f28ec32b97ac7e97f3e4bed31b44907 -Finally, we also use a password to protect your wallet daemon -Your KEY_PASS is not set, do you want to generate one? (y/n) -y - -Done, your daemon password is: 0c0b44096a6f54d3ab92aff019a7dd5ecf383fd0df908e71aec34b62145e96e5 - -[+] Building 11.8s (26/26) FINISHED -............ - -Let's get your wallet daemon address, please wait... -[+] Running 1/1 - ✔ Container platform-daemon-1 Started - -Your wallet daemon address is: efUGPaFNbV3JyqEfv27eRWkQ2tcMd5ympi83p1c2q5VUXPttv -Do you want to start all platform services? (y/n) -y - -[+] Building 179.1s (141/167) -............... - -[+] Running 8/8 - ✔ Container platform-decoder-1 Started - ✔ Container platform-redis-1 Running - ✔ Container platform-daemon-1 Running - ✔ Container platform-database-1 Running - ✔ Container platform-websocket-1 Started - ✔ Container platform-app-1 Started - ✔ Container platform-ingest-1 Started - ✔ Container platform-beam-1 Started - -Your Enjin Platform is now running, please visit: http://localhost:8000/graphiql -``` - -### 4\. Choose Your Network - -For the Enjin Platform Starter, the default network is set to `enjin-matrixchain` (mainnet). You can switch different networks by editing NETWORK variable in the `configs/core/.env` file. -Accepted values are `enjin-matrixchain` / `canary-matrixchain` / `local-matrixchain`. - -:::info To apply the changes, run without caching -When starting the Enjin Platform using the start script, it builds the Docker image with caching. If you've modified the `.env` variables, rebuild using `docker compose build --no-cache` to apply these changes. -::: - -### 5\. Access the Platform - -There are two ways of interacting with Enjin Platform, through the GraphQL API. For testing you can use the GraphiQL playground that will be accessible at http://localhost:8000/graphiql - -![Local Platform GraphiQL Playground](/img/guides/going-open-source/local-platform-graphiql.png) - -But you can also use the Enjin Platform UI which you can access at `http://localhost:8000`, you might see this setup screen where you should enter the `BASIC_AUTH_TOKEN` that was shown to you in the setup script. - -![Setup Configuration](/img/guides/going-open-source/setup-configuration.png) - -![Platform User Interface](/img/guides/going-open-source/platform-ui.png) - -After that, you will be able to navigate through the UI and create collections, tokens, fuel tanks, and everything that the Enjin Platform provides to you. - -### 6\. Make the platform publicly accessible - -Creating a public URL using ngrok - -**Step 1:** Download ngrok from the official website: https://ngrok.com/download - -**Step 2:** Unzip to Install - -On Linux, or macOS you can unzip ngrok from a terminal with the following command. On Windows, just click on ngrok.zip to extract it. - -`$unzip /path/to/ngrok.zip` - -**Step 3:** Connect your account - -Running this command will add your auth token to the default ngrok.yml configuration file. This will grant you access to more features and longer session time. - -`$ngrok config add-authtoken ONE_AUTH_TOKEN_HERE` - -**Step 4:** Running ngrok - -Read the documentation here, Try it out by running it from the command line: - -`$ ngrok help` - -To start an HTTP tunnel forwarding to your local port 8000, run this next: - -`$ ngrok http 8000` - -Once you have run the command from above, the example should appear as the following, where the URL shown is the URL of your project. - -In case you want to access the GraphQL playground, you would just need to add /graphiql at the end of your HTTP - -### 7\. Upgrade to latest version - -**Step 1:** **Update the submodules** by navigating to each of the `platform-decoder` and `wallet-daemon` folders and run: - -```bash -git checkout master && git pull -``` - -**Step 2:** **Rebuild the docker images** with no-cache from the root of the platform: - -```bash -docker compose build --no-cache -``` - -**Step 3:** **Run the container** by running this code from the root of the platform: - -```bash -docker compose up -d -``` - -### 8\. Configure Collections (Optional) - -By default, the Enjin Platform syncs token data from all of the collections in the Blockchain. -To save storage and sync time, you can configure the Enjin Platform to sync only selected collections. -To do that, head over to http://localhost:8000/graphiql and run the `AddToTracked` mutation, with the IDs of the collections you wish to sync, like so: - -```graphql -mutation SyncCollections{ - AddToTracked(type: COLLECTION, chainIds: ["30710", "42100"]) -} -``` diff --git a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/03-faq.md b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/03-faq.md deleted file mode 100644 index 2d8d763..0000000 --- a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/03-faq.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: "FAQ" -slug: "faq" -description: "Find answers to frequently asked questions about the Enjin platform, including setup, integration, and troubleshooting tips." ---- -## Q: How often is the Enjin Platform Updated? - -Updates are scheduled monthly, specifically on the second Tuesday of each month. However, updates may be rolled out sooner if there's a need to support new features, blockchain upgrades, or to address critical security issues. - -## Q: Does the Enjin Platform update automatically? - -No, the Enjin Platform doesn't have an 'automatic' update feature. Instead, the starter project is set to use the stable release branches, which are typically updated manually once a month, on the second Tuesday of the month. Users must update their projects to the latest stable version as it becomes available. - -## Q: What type of data does the database store, and do I need to back it up? - -The database holds both cached data from the blockchain and off-chain data such as Beams. It's advisable to back up your data regularly. Note that tables like `transactions`, `wallets`, `events`, and `pending_events` contain essential data not on the blockchain and should always be backed up. Other tables are mostly caches and can be reconstructed from blockchain data. Also ensure to back up your wallet daemon's seed, password, and file name to recover your setup fully. - -## Q: How do I disable the included wallet daemon and use an external one? - -To disable the internal daemon, remove its configuration from the `docker-compose.yml` file. This will prevent it from starting up with the other services, allowing you to use an external daemon that only permits outgoing connections for enhanced security. Here's a snippet of what you might remove: - -```yaml -daemon: - image: enjin/daemon:latest - build: - context: . - dockerfile: configs/daemon/Dockerfile - restart: unless-stopped - volumes: - - ./configs/daemon/store:/opt/app/storage -``` - -By excluding this section, the internal daemon won't be initialized, and you can then set up an external wallet daemon as needed. diff --git a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/04-enterprise-on-prem.md b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/04-enterprise-on-prem.md deleted file mode 100644 index aa3c93a..0000000 --- a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/04-enterprise-on-prem.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -title: "Enterprise On-Prem" -slug: "../enterprise-on-prem" -description: "Find out how to set up enterprise on-prem for your blockchain infrastructure, giving you control over your environment and data security." ---- -:::info Choosing the right platform for you -There are two version of the Enjin Platform -- The [Enjin Platform Cloud](http://platform.enjin.io/) - - A cloud-hosted version that you can start using straight away. - - Ideal if you're new to Enjin or don't want to manage your own infrastructure. -- The [ Enterprise On-Prem Enjin Platform](https://github.com/enjin/platform) - - A self-hosted solution that offers customization and full control over the software. - - Best for those who are more tech-savvy. It gives you full control over the data and source code, allowing you to customize the platform extensively, including adding new features, writing your own Beam conditions or creating custom events. However, running it requires systems administration skills. You'll need to handle backups, updates, server maintenance, and set up internet access and SSL certificates for the platform. -::: - -## Advantages of Running the Enjin Platform On-Premises (Enterprise) - -Enterprise On-Prem offers significant benefits for app development: - -- **Seamless Transactions**: Includes transaction retries, wallet nonce management, and gas estimation for smooth operations. -- **Comprehensive Web3 Tools**: Offers a blockchain indexer, GraphQL API, and other vital infrastructure for efficient blockchain interaction. -- **Reliable Server Performance**: Features a production-grade HTTP server, ensuring robust app hosting. -- **Scalability and Flexibility**: Highly scalable, supports millions of users, and fully customizable to meet specific needs. -- **Efficient Data Handling**: Fast blockchain data retrieval with advanced filtering, and real-time updates through subscriptions to events. -- **Automated Processes**: Supports automated wallet management and asset transfers, streamlining user interactions. -- **Enhanced Data Integrity**: Includes quick sync and auto-healing features for maintaining up-to-date and accurate database information. - -## Real-time Platform Architecture - -The package consists of three main components: - -### Indexer - -Our package includes a blockchain indexer that quickly retrieves blockchain data, speeding up the process compared to direct chain queries. It also allows for creating relationships and filtering data in ways not possible with direct queries. - -### APIs - -Our GraphQL API offers a server-based interface for a variety of queries and mutations, enabling easy access to information and transaction execution on the blockchain. - -### Events - -The Enjin Platform allows developers to subscribe to events for real-time updates on blockchain activities, reducing the need for constant API checks. This simplifies blockchain interactions by eliminating complex tasks like encoding and transaction signing. - -## Understandig the API and Indexer - -The Enjin Platform uses an indexer for fast blockchain data retrieval, bypassing slower direct queries. It employs GraphQL as its main API, allowing developers to efficiently access only the data they need, which enhances performance and reduces bandwidth. - -Enabled Automation - -- Request for user proof; -- Authentication request; -- Creating managed wallets; -- Wallet linking and switching; -- Asset transfer and management; -- Enjin Beam support; -- Fuel Tanks support; -- Marketplace support. - -The Enjin Platform indexer is designed to minimize the blockchain's storage and database growth. Indexers organize data, making it easier for developers to retrieve the necessary information. - -### Data Management Tool - -The Data Management Tool from Indexers organizes and structures blockchain data. - -It creates a centralized database, making it easier for game developers to access specific data they need without sorting through all blockchain data. - -### Quick Sync Method - -The Enjin Platform features a quick sync method allowing developers to rapidly update and store the latest blockchain state using a pre-packaged index. - -This method saves time by avoiding full blockchain syncing. - -A "Worker" service manages and processes all blockchain blocks and their associated events. - -### Auto-Healing Feature - -The Enjin Platform has an auto-healing feature that constantly checks the database's health. - -If it finds problems like corruption or mismatches, it automatically updates to match the current blockchain state, keeping data accurate and current. - -### Events & WebSockets - -The Enjin Platform sends notifications through WebSocket channels about activities on the platform. - -For instance, it alerts when a new collection is created. Clients can get these updates by subscribing their wallets to the WebSocket channel via the Enjin Wallet Daemon. - -This daemon not only checks for transactions to sign but also listens for these event notifications, enabling real-time interaction and dynamic experiences on the platform. \ No newline at end of file diff --git a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/04-laravel-horizon.md b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/04-laravel-horizon.md deleted file mode 100644 index db91192..0000000 --- a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/04-laravel-horizon.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: "Laravel Horizon" -slug: "laravel-horizon" -description: "Discover how to use Laravel Horizon to manage and monitor queue workloads for the Enjin platform, ensuring smooth operations." ---- -The Enjin Platform's new architecture has improved how data is handled, including retrieval, processing, and storage. - -Key updates include: - -- **Processor Container**: A new container decodes information and stores it in your local database. - -- **Monitoring with Laravel Horizon**: Laravel Horizon is integrated for monitoring this processor container. It helps in tracking the status of jobs and pinpointing any failures. - -- **Dashboard Access**: To view the processor container's activities, visit the Laravel Horizon dashboard at: http://localhost:8000/horizon/dashboard. - -For more details on Laravel Horizon, refer to their documentation: https://laravel.com/docs/10.x/horizon. - -![Laravel Horizon Dashboard](/img/guides/going-open-source/laravel-dashboard.png) diff --git a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/05-extending-the-platform.md b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/05-extending-the-platform.md deleted file mode 100644 index 573a537..0000000 --- a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/05-extending-the-platform.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -title: "Extending the Platform" -slug: "extending-the-platform" -description: "Learn how to extend the Enjin platform by adding custom features and integrations to meet the unique needs of your blockchain projects." ---- -Our platform lets you add your own code to customize it without changing the original code. For example, you can use the `adhocRules` feature in the Beam Mutation to set custom rules. - -One way to use this is by adding a `ValidationRule` to the `ClaimBeamMutation`. This rule limits how many times a user can claim a Beam, based on their IP address and wallet account. It ensures a user can only claim once per IP or account. - -When a Beam is claimed, we save the user's IP and wallet account in the database. Using this, we can check if a claim has already been made from that IP or account. If it has, the claim is denied. - -First, we create a Laravel `ValidationRule`. Then, we add code to check the `beam_claims` table for the user's IP or account. If it's already there, the claim fails and an error message is shown. This check happens early in the process, stopping claims before they go too far. - -```php -data = $data; - - return $this; - } - - public function validate(string $attribute, mixed $value, Closure $fail): void - { - $code = Arr::get($this->data, 'code'); - $cap = 1; - - if (BeamClaim::hasCode($code) - ->where( - fn ($builder) => $builder - ->where('wallet_public_key', $value) - ->orWhere('ip_address', request()->getClientIp()) - ) - ->count() >= $cap) { - - $fail($this->message()); - } - } -} -``` - -In the code, we use a fixed limit (`$cap`) of 1 for claims, but you can make this adjustable by using an environment variable (ENV). This avoids the need for new builds when changing the cap. - -After creating the validation rule, add it to the `ClaimBeamMutation` by placing it in the `adhocRules` array. Do this in the `boot()` method of your app's `AppServiceProvider` class. This setup ensures the rule checks the supplied `account` field and its data. - -```php -public function boot() -{ - ClaimBeamMutation::$adhocRules = ['account' => [new ClaimBeamPerUser()]]; -} -``` - -This example shows how to add custom rules for extra checks in the Beam package without modifying its core code. You can also include more checks beyond just the user's IP address and wallet account if needed. diff --git a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/_Telemetry_02-local-installation.md b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/_Telemetry_02-local-installation.md deleted file mode 100644 index 4775f91..0000000 --- a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/_Telemetry_02-local-installation.md +++ /dev/null @@ -1,197 +0,0 @@ ---- -title: "Local Installation" -slug: "local-installation" -description: "Learn how to install Enjin's blockchain tools locally on your system, enabling you to develop and test your applications offline or in a controlled environment." ---- -## Running it locally - -The [Enterprise On-Prem Enjin Platform](https://github.com/enjin/platform) was made to be as simple as possible to run, you will be able to have a full running environment in less than 5 minutes. - -### 1\. Install Dependencies - -We have two dependencies that must be installed in your server or computer, git, a command line version control system, you can download it here: https://git-scm.com/downloads - -And docker, you may install either the [docker engine](https://docs.docker.com/engine/install/) or [docker desktop](https://www.docker.com/products/docker-desktop), though for people that have never used docker before we recommend using docker desktop: https://www.docker.com/products/docker-desktop - -### 2\. Clone the Repository - -In order for this to work you have to clone the repository, please note that downloading it as a zip will generate problems with the submodules. - -To clone the repository just run the following command in the Terminal or Command Prompt in the folder where you want to download the platform. - -```bash -git clone https://github.com/enjin/platform -``` - -:::info Logging in -You login by editing the `configs/core/.env file`. Here, you use the `DAEMON_ACCOUNT` and `BASIC_AUTH_TOKEN` environment variables for the wallet account and API token, respectively. -::: - -### 3\. Run the Start Script - -In the main folder of the platform, there are two start scripts: `start.bat` for Windows and `start.sh` for macOS and Linux. - -Running either script sets everything up automatically. - -#### Running the Start script on Windows - -To run the Start script on windows, simply double click the `start.bat` file. - -#### Running the Start script on MacOS / Linux - -To run the Start script on MacOS / Linux, open your Terminal application on MacOS, or establish an SSH connection to your Linux server. -Then, navigate to the platform's root directory using the `cd platform` command, and start the script using the following command: - -```bash -./start.sh -``` - -*** - -A sample output is available to show what happens during this process. - -```text -Welcome to Enjin Platform, this script will help you start it up -For a few things to work, we need to know your platform URL -Please input the URL: (e.g. http://127.0.0.1) -http://127.0.0.1 - -Laravel uses an app key to protect your data with encryption -Your APP_KEY is not set, do you want to generate one? (y/n) -y - -Done, your key is: base64:CGAdVfzKICzydRuipAxt9UxlbfMjggDiiT52T7P9H0A= -We also use a static token to protect your platform from unauthorized access -Your BASIC_AUTH_TOKEN is not set, do you want to generate one? (y/n) -y - -Done, your basic static token is: 01f093ba63b4285b0ec451f406e3d8580f28ec32b97ac7e97f3e4bed31b44907 -Finally, we also use a password to protect your wallet daemon -Your KEY_PASS is not set, do you want to generate one? (y/n) -y - -Done, your daemon password is: 0c0b44096a6f54d3ab92aff019a7dd5ecf383fd0df908e71aec34b62145e96e5 - -[+] Building 11.8s (26/26) FINISHED -............ - -Let's get your wallet daemon address, please wait... -[+] Running 1/1 - ✔ Container platform-daemon-1 Started - -Your wallet daemon address is: efUGPaFNbV3JyqEfv27eRWkQ2tcMd5ympi83p1c2q5VUXPttv -Do you want to start all platform services? (y/n) -y - -[+] Building 179.1s (141/167) -............... - -[+] Running 8/8 - ✔ Container platform-decoder-1 Started - ✔ Container platform-redis-1 Running - ✔ Container platform-daemon-1 Running - ✔ Container platform-database-1 Running - ✔ Container platform-websocket-1 Started - ✔ Container platform-app-1 Started - ✔ Container platform-ingest-1 Started - ✔ Container platform-beam-1 Started - -Your Enjin Platform is now running, please visit: http://localhost:8000/graphiql -``` - -### 4\. Choose Your Network - -For the Enjin Platform Starter, the default network is set to `enjin-matrixchain` (mainnet). You can switch different networks by editing NETWORK variable in the `configs/core/.env` file. -Accepted values are `enjin-matrixchain` / `canary-matrixchain` / `local-matrixchain`. - -:::info To apply the changes, run without caching -When starting the Enjin Platform using the start script, it builds the Docker image with caching. If you've modified the `.env` variables, rebuild using `docker compose build --no-cache` to apply these changes. -::: - -### 5\. Access the Platform - -There are two ways of interacting with Enjin Platform, through the GraphQL API. For testing you can use the GraphiQL playground that will be accessible at http://localhost:8000/graphiql - -![Local Platform GraphiQL Playground](/img/guides/going-open-source/local-platform-graphiql.png) - -But you can also use the Enjin Platform UI which you can access at `http://localhost:8000`, you might see this setup screen where you should enter the `BASIC_AUTH_TOKEN` that was shown to you in the setup script. - -![Setup Configuration](/img/guides/going-open-source/setup-configuration.png) - -![Platform User Interface](/img/guides/going-open-source/platform-ui.png) - -After that, you will be able to navigate through the UI and create collections, tokens, fuel tanks, and everything that the Enjin Platform provides to you. - -### 6\. Make the platform publicly accessible - -Creating a public URL using ngrok - -**Step 1:** Download ngrok from the official website: https://ngrok.com/download - -**Step 2:** Unzip to Install - -On Linux, or macOS you can unzip ngrok from a terminal with the following command. On Windows, just click on ngrok.zip to extract it. - -`$unzip /path/to/ngrok.zip` - -**Step 3:** Connect your account - -Running this command will add your auth token to the default ngrok.yml configuration file. This will grant you access to more features and longer session time. - -`$ngrok config add-authtoken ONE_AUTH_TOKEN_HERE` - -**Step 4:** Running ngrok - -Read the documentation here, Try it out by running it from the command line: - -`$ ngrok help` - -To start an HTTP tunnel forwarding to your local port 8000, run this next: - -`$ ngrok http 8000` - -Once you have run the command from above, the example should appear as the following, where the URL shown is the URL of your project. - -In case you want to access the GraphQL playground, you would just need to add /graphiql at the end of your HTTP - -### 7\. Upgrade to latest version - -** Step 1:** **Update the submodules** by navigating to each of the `platform-decoder` and `wallet-daemon` folders and run: - -```bash -git checkout master && git pull -``` - -**Step 2:** **Rebuild the docker images** with no-cache from the root of the platform: - -```bash -docker compose build --no-cache -``` - -**Step 3:** **Run the container** by running this code from the root of the platform: - -```bash -docker compose up -d -``` - -### 8\. Configure Collections (Optional) - -By default, the Enjin Platform syncs token data from all of the collections in the Blockchain. -To save storage and sync time, you can configure the Enjin Platform to sync only selected collections. -To do that, head over to http://localhost:8000/graphiql and run the `AddToTracked` mutation, with the IDs of the collections you wish to sync, like so: - -```graphql -mutation SyncCollections{ - AddToTracked(type: COLLECTION, chainIds: ["30710", "42100"]) -} -``` - -### 9\. Configure Telemetry (Optional) - -The Enjin Platform includes a telemetry feature that periodically collects and provides data about your platform instance. This data includes information such as package versions, host URL, configured blockchain, operating system, PHP version, and more. By default, this feature is enabled. If you prefer to disable telemetry, set the `TELEMETRY_ENABLED` environment variable to `false`. - -Additionally, you can choose to provide an email address to receive important notifications. These notifications may alert you to breaking changes, incompatible package versions, or other critical updates related to your platform configuration. To enable this, set the `UPGRADE_NOTIFICATION_EMAIL` environment variable to your preferred email address. You can remove your email address at any time by setting the `UPGRADE_NOTIFICATION_EMAIL` environment variable to `null`. - -:::note -Your email address will only be collected if the `TELEMETRY_ENABLED` environment variable is set to `true`. -::: \ No newline at end of file diff --git a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/_category_.json b/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/_category_.json deleted file mode 100644 index 353520f..0000000 --- a/versioned_docs/version-2.x/05-enjin-platform/04-enterprise-on-prem/_category_.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "label": "Enterprise On-Prem" -} \ No newline at end of file diff --git a/versioned_docs/version-2.x/05-enjin-platform/05-enjin-platform.md b/versioned_docs/version-2.x/05-enjin-platform/05-enjin-platform.md deleted file mode 100644 index 9f88720..0000000 --- a/versioned_docs/version-2.x/05-enjin-platform/05-enjin-platform.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: "Enjin Platform" -slug: "../enjin-platform" -description: "Discover the Enjin Platform, a comprehensive suite of tools for building, managing, and integrating blockchain-based assets in your projects." ---- -:::info There are three versions of the Enjin Platform to choose from: -- The [Enjin Platform Cloud (Testnet)](https://platform.canary.enjin.io/login), connected to Enjin Canary Testnet -- The [Enjin Platform Cloud (Mainnet)](http://platform.enjin.io/), connected to Enjin Blockchain Mainnet -- The [Enterprise On-Prem Enjin Platform](https://github.com/enjin/platform) where you can choose to connect to either network. -::: - -## What is the Enjin Platform? - -The Enjin Platform is the world’s most powerful and advanced open-source framework for building NFT platforms and integrations. It allows developers and their projects to easily communicate with the Enjin Blockchain, forming a light-weight and modular layer between the blockchain, and their applications. - -It's the bridge between the complex world of blockchain and user-friendly applications, minimizing the technical barriers usually associated with blockchain integration. This is achieved through a lightweight, modular layer that uses a modern, flexible GraphQL API. This layer manages all blockchain interactions, thus removing the necessity of engaging with traditional smart contracts directly. - -Consider the Enjin Platform as your digital interpreter, simplifying your communication with the blockchain. It enables you to create collections, mint and transfer blockchain tokens, and even administer blockchain collections and wallets with ease. - -The Enjin Platform works much like a customizable building set. It has a modular design, which means you can add or take away pieces as needed. These pieces come in the form of optional add-on packages. They provide extra tools, such as managing Fuel Tanks, handling marketplace listings (through the marketplace package), or generating Enjin Beams via the API (with the Enjin Beam package). In other words, with the Enjin Platform, you get a customized setup that fits your specific needs. - -This versatile platform offers two versions to cater to different needs. The [Enterprise On-Prem](https://github.com/enjin/platform), self-hosted version provides you with comprehensive control over your data, eliminating dependence on any third-party servers or services. Enjin also offers a hosted version, the Enjin Platform Cloud ([mainnet ](https://platform.enjin.io/)\|[ canary](https://platform.canary.enjin.io/)), tailored for users seeking rapid integration. You can choose the version that best fits your requirements. - -![](/img/components/4.png) - -## How does it work? - -How does Enjin Platform bring all these functionalities together? The answer lies in its structure. The platform comprises distinct components each designed to offer a unique solution for developers and creators. - -

- -

- -### API & Schema - -The [Open-Source Enjin Platform](https://github.com/enjin/platform) also provides a ready-made application that simplifies blockchain actions for developers by removing the need for encoding, signing transactions, and understanding complex technical details. - -The project has two key components: - -- **Enjin API:** Our GraphQL API provides a server-based interface that allows you to execute a range of queries and mutations. In other words, you can easily retrieve necessary information or execute transactions on the blockchain. -- **Events:** With the Enjin Platform, developers can subscribe to events and receive real-time updates on any on-chain activities, eliminating the need to constantly poll the API for changes. - -### Wallet Daemon - -The [Wallet Daemon](https://github.com/enjin/wallet-daemon) is the component responsible for automatically signing and broadcasting transactions to the Enjin Blockchain. Running on your own secure server, the Wallet Daemon removes the need for manual signing of each transaction by polling the Enjin Platform in 6-second intervals, fetching new transaction requests, signing them with your private key, and submitting them to a blockchain node. As the Wallet Daemon keeps track of the nonce locally, it can process and submit more than one transaction per block. - -### SDKs - -With the release of the Enjin Platform, we’re preparing multiple SDKs to further streamline integration into your projects. Our first release includes the open-source [C# SDK](https://github.com/enjin/platform-csharp-sdk) for connecting and interacting with the Enjin Platform. - -Want to learn more about our SDKs? Dive into our documentation [here](/02-guides/01-platform/04-software-development-kit/04-software-development-kit.md). - -## Notable Platform Features - -The Enjin Platform is packed with features designed to make your life as a developer easier. Here's a glimpse of what's in store: - -### Enabled Automation - -- **User-validation requests:** A swift way to establish user identities -- **Authentication requests:** Seamless process to secure sessions -- **Managed wallet creation:** Forget the complex coding, create wallets with a few clicks -- **Wallet linking and switching:** Easily manage multiple wallets and switch between them -- **Asset transfer and management:** Smoothly transfer and manage your digital assets -- **Enjin Beam support:** Leverage Enjin Beams for unique offerings -- **Fuel Tanks support:** Efficiently manage your fuel tanks for subsidizing transaction fees -- **Marketplace support:** Create and manage marketplace listings with ease -- **Relaychain auto-teleport:** Any Relaychain ENJ mistakenly sent to managed wallet will be automatically teleported to Matrixchain - -### Data Management Tool - -Simplify data organization with our Data Management Tool. Provided by Indexers, it structures and organizes blockchain data by creating a centralized database from blockchain inputs. This means you, as a developer, can access specific data without sifting through the entire blockchain database. - -### Quick Sync Method - -The Enjin Platform offers a quick sync method that enables developers to fetch and store the most recent state of the blockchain in a pre-packaged index. This saves time and effort that would otherwise be spent on syncing the entire blockchain. The "Worker" service oversees and manages all the blocks produced in the blockchain and parses all extrinsic events in each block. - -### Auto-Healing Feature - -The Enjin Platform includes an auto-healing feature that continuously monitors the health of the database. If any issues arise, such as corruption or a saving mismatch, the platform automatically re-syncs with the current chain state to ensure that all data is correct and up-to-date. - -### Relaychain ENJ Auto-teleport - -The Enjin Ecosystem is composed of two distinct chains: the Enjin Relaychain, which handles Staking and Governance, and the Enjin Matrixchain, a hub for NFTs and marketplace functionalities. There can be a potential misunderstanding when users purchase ENJ on an exchange and deposit it into a Dapp's managed wallet on the Relaychain. This is because the Dapp creator anticipates the deposit on the Matrixchain, not the Relaychain. To address this, we've introduced the Auto-Teleport feature. This feature automatically teleports any Relaychain ENJ received in a managed wallet to the corresponding Matrixchain. It streamlines the process, eliminates room for error and the need for manual teleportation. - -### Modular Design - -The Enjin Platform is designed with a light, modular structure, allowing developers to custom-fit the integration depending on their requirements and design philosophy. The Platform and Platform Core provide fundamental functionality, while multiple optional packages are available to expand your integration. You can enhance the Platform's capabilities without modifying the core codebase. - -Developers may further augment their Enjin Platform configuration by integrating [Laravel Horizon](https://laravel.com/docs/10.x/horizon), translating all Platform data into a visually appealing dashboard. - -Everyone is welcome to contribute to the codebase and build upon the framework. Don’t forget to [Follow Enjin on GitHub](https://github.com/enjin)! - -### Platform User Interface - -The Enjin Platform comes with a lightweight and easy-to-use user interface. The graphical interface comes with GUI forms for creating, viewing, managing collections & NFTs. You can also extend the UI with additional components like Beams and Fuel Tanks, depending on their availability and installation. - -![](/img/components/6.png) - -The user interface is available as a [standalone application](https://github.com/enjin/platform-ui) for the Enterprise On-Prem Enjin Platform, but comes standard when using the Enjin-hosted implementation of the [Enjin Platform](https://platform.enjin.io/). - -## Enjin Platform Cloud - -The modularity and flexibility of the Enjin Platform can be daunting for first-timers - which is exactly why we’ve rolled out the Enjin Platform Cloud! - -This user-friendly version allows you to kickstart your next NFT project in just a few minutes. Ran on Enjin's robust servers, yet fully secure and isolated to your unique project instance, you can launch your NFT game with a few simple steps. Just create an account, set it up, generate your first API key, and voila! Your NFT game is operational. The [Wallet Daemon](https://github.com/enjin/wallet-daemon) on your own server securely handles the signing and broadcasting of all transactions, ensuring the utmost integrity of your project's security. - -Get started today on [mainnet](https://platform.enjin.io/register) or [canary](https://platform.canary.enjin.io/register). diff --git a/versioned_docs/version-2.x/05-enjin-platform/_category_.json b/versioned_docs/version-2.x/05-enjin-platform/_category_.json deleted file mode 100644 index be030c0..0000000 --- a/versioned_docs/version-2.x/05-enjin-platform/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Enjin Platform" -} - \ No newline at end of file diff --git a/versioned_docs/version-2.x/06-enjin-products/01-enjin-tech-stack.md b/versioned_docs/version-2.x/06-enjin-products/01-enjin-tech-stack.md deleted file mode 100644 index 4f97b75..0000000 --- a/versioned_docs/version-2.x/06-enjin-products/01-enjin-tech-stack.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: "Enjin Tech-Stack" -slug: "enjin-tech-stack" -description: "Explore the Enjin Ecosystem, providing innovative blockchain solutions for developers, enabling the integration of NFTs, tokens, and decentralized applications." ---- -Enjin is the ideal blockchain platform for your game or business due to its gaming roots and the powerful Relaychain infrastructure that it's built upon. The Enjin ecosystem comprises the Enjin Platform, NFT.io, Enjin Wallet 2.0, Beam, SDKs, and other tools and services that allow game and app developers to boost their revenue, gain a competitive advantage, and innovate in ways that were previously impossible. - -![](/img/components/1.webp) - -Enjin's tools and services, originally developed for gaming, are versatile and applicable to businesses of all sizes and industries. Whether seeking to create blockchain products or utilize tokenized digital assets for acquisition, retention, engagement, and monetization strategies, Enjin's expertise can be a valuable resource. -The Enjin Platform is a powerful Platform as a Service (PaaS) for blockchain technology, designed to simplify the creation and administration of blockchain games. - -Unlike traditional blockchain game development that requires building and maintaining complex infrastructure, the Enjin Platform offers a comprehensive, and adaptable set of tools and services. With Enjin's Blockchain SDKs, Platform API, and Automation services such as the wallet Daemon, game developers can revolutionize the creation of blockchain games. The Enjin Platform also incorporates Enjin Relaychain and Enjin Matrixchain, which serve as the blockchain backbone of the entire system. - -

- -

- -The Enjin Ecosystem provides numerous advantages, one of which is the ability to leverage innovative crowdfunding models. With this platform, you can create gaming assets such as characters, weapons, and real estate ahead of time and offer them to gamers through conventional crowdfunding platforms or a custom website powered by NFT.io. Using Beam, the world's first QR-based blockchain asset distribution service, assets can be easily shared with QR codes. Furthermore, you can facilitate user-driven value creation by enabling players to modify, craft, upgrade, build, and trade blockchain-based gaming assets, which can gain intrinsic value based on their history, stats, utility, and customization. - -Enjin's Ecosystem provides benefits beyond just cutting-edge crowdfunding models, including the establishment of user loyalty and trust through true ownership of digital gaming assets. This ownership leads to more loyal users who are more likely to make lost revenue by eliminating gray market trading, which can account for up to 40% of lost revenue. Furthermore, player churn can be reduced by ensuring gaming assets are owned by gamers and safely stored in their private Enjin Wallet, which requires approval for each transaction, thus making hacking or in-game fraud impossible. - -

- -

- -The Enjin Platform's versatility extends beyond gaming to a variety of other use cases. For instance, you can transform artwork into NFTs, allowing for easy display in virtual galleries or as proof of ownership for physical pieces. - -New technology allows you to produce music and convert it into a powerful token with exclusive rights. You can also tokenize physical collectibles like vintage comics, rare paintings, and real estate. - -Furthermore, you can design unique, easily tradeable, and programmable digital collectibles that convey a compelling narrative and leverage the Enjin Platform's 3D modeling support to create a fully immersive VR and AR experience with tokenized assets. diff --git a/versioned_docs/version-2.x/06-enjin-products/02-enjin-coin.md b/versioned_docs/version-2.x/06-enjin-products/02-enjin-coin.md deleted file mode 100644 index f48fe73..0000000 --- a/versioned_docs/version-2.x/06-enjin-products/02-enjin-coin.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: "Enjin Coin" -slug: "enjin-coin" -description: "Learn about Enjin Coin, the cryptocurrency powering blockchain-based assets and transactions within the Enjin Ecosystem, from games to applications." ---- - -import GlossaryTerm from '@site/src/components/GlossaryTerm'; - -Enjin Coin (ENJ) is the native coin of the Enjin Blockchain, serving as the main token for performing actions and facilitating transactions on the network (staking, marketplace, governance, etc.) -It is used to pay  (also known as ), , and to add to s on the . - -Enjin can come in three different forms: - -- **:** This is the native version of Enjin Coin. You can buy it on cryptocurrency exchanges and use it for each or use it to create new s on the Enjin Blockchain . -- **cENJ:** This is the version of Enjin Coin. It's available for free from the [cENJ faucet](https://faucet.canary.enjin.io/) and can be used just like ENJ for transactions and minting s, but on the Enjin Canary Testnet. -- **sENJ:** sENJ represents staked ENJ. It's issued to you when you [stake your ENJ](/02-guides/02-blockchain/01-relaychain/04-joining-nomination-pools.md) and is destroyed when you unstake. You can your ENJ into a or . Once you've received your sENJ from staking, it can be [traded for ENJ](/04-enjin-blockchain/02-enjin-relaychain/02-stake-exchange-pallet.md). - -## ENJ Infusion - -Infusing ENJ to a is the process of locking Enjin Coin (ENJ) inside each unit as its backing value. - -Here's why it's important: - -- The holder can melt it at anytime to retrieve the embedded ENJ. This provides intrinsic value for s, bringing value besides it's market value. -- Once a is infused, the infused amount is required to be deposited for each new unit minted, making the asset more scarce. - -It’s important to note that once a is infused with ENJ, it's infused ENJ amount can increase but it can never be decreased. - -### Minimum Account Balance (Existential Deposit) - -To keep an Enjin Coin Account active on the network, the wallet must hold **at least 0.1 ENJ**. If the balance drops below this threshold, the account may be **automatically deleted** - unless the transfer uses the `keepAlive` flag set to `true`. - -:::note -This requirement only applies to Enjin Coin accounts. NFT () holdings are unaffected and remain safe even if the wallet has 0 ENJ. -::: - -When `keepAlive` is set to `true`, the transfer will fail if it would reduce the balance below 0.1 ENJ—ensuring the account stays active. If set to `false`, the transfer will go through, but the account may be deleted if the balance falls below the threshold. - -Use `keepAlive: true` to **prevent accidental deletion**, especially when transferring most or all of a wallet’s balance. - -### Token Account Deposit - -A of 0.01 ENJ is required for each holder. -This deposit is also known as . -Once s are destroyed, or 'burned', this deposit is returned to the issuer's account proportionally. diff --git a/versioned_docs/version-2.x/06-enjin-products/03-enjin-wallet.md b/versioned_docs/version-2.x/06-enjin-products/03-enjin-wallet.md deleted file mode 100644 index 017ac2d..0000000 --- a/versioned_docs/version-2.x/06-enjin-products/03-enjin-wallet.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: "Enjin Wallet" -slug: "enjin-wallet" -description: "Discover the Enjin Mobile Wallet, enabling users to manage blockchain assets, including tokens and NFTs, securely from their smartphones." ---- -## What is the Enjin Wallet? - -The Enjin Wallet underwent a complete overhaul, resulting in enhanced performance, stability, and reliability. It now boasts improved power, speed, and user-friendliness, making it an excellent option for those interested in a blockchain wallet tailored specifically to NFTs, such as game developers, art collectors, and gamers. - -The Enjin Wallet's updated user interface and designs ensure accessibility to users of all backgrounds, providing a secure and seamless solution for storing, trading and transferring NFTs. Additionally, the wallet offers extensive compatibility with multiple cryptocurrencies for storage purposes. - -## Enjin Wallet 2.0 Demo -
- -
- -Game developers can enjoy seamless integration with the Enjin Platform when using the new Enjin Wallet, as it is fully compatible with the Enjin Blockchain, our API, SDKs, and NFT.io support. This makes the Enjin Wallet an ideal digital wallet for storing, trading and transferring NFTs in a secure and seamless way. With its user-friendly interface and broad cryptocurrency compatibility, the Enjin Wallet is a valuable tool for anyone looking to engage with the Enjin ecosystem. - -The Enjin Wallet is not limited to the Enjin Blockchain and also supports a wide range of other blockchains, including BTC, LTC, ETH, BEP20, Polygon, Polkadot, KSM, Doge, ERC20, and other standards. This broad spectrum of compatibility ensures that users can store, trade, and manage a diverse range of digital assets all in one place. -The Enjin Wallet is available for both Android and iOS devices: - -Download from [Google Play](https://play.google.com/store/apps/details?id=com.enjin.mobile.wallet&hl=en&gl=US&pli=1) - -Download from the [App Store](https://apps.apple.com/us/app/enjin-nft-crypto-wallet/id1349078375) diff --git a/versioned_docs/version-2.x/06-enjin-products/04-nftio-marketplace.md b/versioned_docs/version-2.x/06-enjin-products/04-nftio-marketplace.md deleted file mode 100644 index 88f4272..0000000 --- a/versioned_docs/version-2.x/06-enjin-products/04-nftio-marketplace.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: "NFT.io Marketplace" -slug: "nftio-marketplace" -description: "nft-io-marketplace" ---- -## What is NFT.io? - -[NFT.io](http://nft.io/) is the latest innovation from the Enjin team, a global NFT Marketplace fully integrated with the Enjin Blockchain. As the next-generation NFT blockchain, Enjin provides a robust and secure foundation for [NFT.io](http://nft.io/), offering a smart, user-friendly, and ad-free platform for all NFT enthusiasts. - -![](/img/components/7.png) - -At [NFT.io](http://nft.io/), you can easily explore digital art, crypto-collectibles, and other NFTs through a simple and accessible marketplace. Whether you're listing your favorite NFTs, bidding on special edition assets, or creating your own collections, NFT.io offers a seamless experience. Our vision is to empower you to discover, showcase, and trade NFTs in a way that reflects your unique passions and interests. So join us on [NFT.io](https://nft.io/) and experience the future of NFT marketplaces today. - -## Key Benefits & Features - -- **Connect using your very own wallet:** The platform is seamlessly integrated with the Enjin Wallet. - No Ads: Enjoy an ad-free experience on NFT.io. You can explore the platform without any interruption from pesky promotions, dangerous scams, or annoying ads. -- **Beginner Friendly:** NFT.io has been revamped from scratch, boasting a user-friendly interface that caters to people from diverse backgrounds and skill levels. Whether you're an experienced NFT trader or a beginner, NFT.io offers simplicity and ease of use for all. -- **Made for all NFTs:** NFT.io is designed to support all types of NFTs. Whether you're an art collector, a music enthusiast, a fan of crypto collectibles, or simply browsing, we cater to all NFT categories. -- **Responsive Design:** NFT.io has a responsive design that's optimized for mobile support, enabling you to access the NFT marketplace even when you're on the go. The user interface is designed to adjust automatically to fit the screen size of your device, from widescreen 4k resolution desktop monitors to smartphones and tablets of all sizes. -- **Instant listings and purchases:** Instantly list and purchase NFTs with just a few clicks. Transactions are completed within milliseconds, allowing for a smooth and efficient experience. -- **Auctions and Bidding:** You can easily set up auctions for your favorite NFTs or collectibles and place your bids. -- **Create Enjin Beams:** Leverage the power of Enjin's QR distribution system to create Enjin Beams. -- **Credit card/Fiat payment support:** The NFT.io marketplace offers a seamless integration of credit card and fiat payment methods, making it easy to purchase beam credits on the platform. -- **Full GIF/MP4/GLB File Support:** The NFT.io marketplace fully supports both GIF and MP4 files, allowing you to flawlessly showcase your 3D NFTs. -- **Receive Instant Notifications:** Get immediate notifications and email updates whenever your assets are listed, sold, or receive a bid, so you can stay on top of all your transactions diff --git a/versioned_docs/version-2.x/06-enjin-products/05-beam.md b/versioned_docs/version-2.x/06-enjin-products/05-beam.md deleted file mode 100644 index dad3878..0000000 --- a/versioned_docs/version-2.x/06-enjin-products/05-beam.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: "Enjin Beam" -slug: "beam" -description: "Discover how to use Enjin Beam to distribute blockchain assets through QR codes, making it easy to send and receive NFTs and tokens." ---- - -:::info Explore Our Comprehensive Beam Guide -For a detailed walkthrough of Enjin Beam's features and how to use them, check out our [in-depth guide for beams](/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md). -::: - -## What is Enjin Beam? - -Enjin Beam offers an efficient way to market, promote, and distribute blockchain assets. By leveraging QR code technology, Enjin Beam allows for the easy distribution of blockchain assets on the Enjin blockchain. This feature is available on NFT.io and can be used by anyone who registers on the marketplace. - -Moreover, game developers can access Enjin Beam through the Enjin Platform API, which is designed to automate the distribution process. On the other hand, [NFT.io](https://nft.io/) is intended for users who prefer a more hands-on approach. - -### Here's how Enjin Beam works: - -1. First, blockchain assets are created on NFT.io or through the Enjin Platform API. -2. The assets are then placed in a smart contract, acting as a "vault", using the Beam interface. This generates a unique QR code. -3. Users can scan the code with the [Enjin Wallet](https://enjin.io/products/wallet) app and receive the blockchain asset directly into their wallet. -4. If the user does not have the Enjin Wallet installed, they can still interact with the Enjin Web Wallet interface to claim their blockchain assets using Enjin Beam. - -### Send Anything; Post Anywhere - -Enjin Beam offers a flexible way to send rewards that can be customized to fit your target audience and marketing goals. Whether it's gaming assets, branded collectibles, or early access, Enjin Beam lets you personalize your rewards to meet your needs. - -Beam QR codes can be shared through various visually accessible platforms, including social media networks, websites, ads, videos, flyers, presentations, and even within apps or games. This allows you to reach a broad audience and get your rewards in front of the right people. - -**Enjin Beam enables you to:** - -1. Simplify the creation and generation of QR codes with a few straightforward steps. -2. Distribute NFTs on a large scale via a QR code, making it easy for users to scan and claim. -3. Enhance your online presence and offer a user-friendly experience that appeals to a wide range of users, including those who are not familiar with crypto or blockchain technology. - -![](/img/components/8.png) - -:::tip Master Enjin Beam -Want to unlock the full potential of your NFT distributions? Our comprehensive guide to Enjin Beam offers advanced techniques, detailed setup instructions, and strategic insights for maximizing your campaign's reach and impact. -➡️ **[Read the In-Depth Guide to Enjin Beam](/02-guides/01-platform/01-managing-tokens/06-create-qr-drops.md)** -::: - -## Types of Beams - -### Mint on Demand - -One of the significant differences between Enjin Beam and its predecessors is the `MINT_ON_DEMAND` feature. This feature eliminates the need to pre-mint all tokens for distribution, which is the default way of creating beams on Enjin Blockchain. With this feature, Beam creators can define a parameter on the mutation for tokens to be minted directly to users' wallets at the exact moment they are claimed, reducing the time people wait for the tokens to arrive in their wallets. - -![](/img/components/9.png) - -### Batch Mint & Batch Transfer - -Enjin Beam also introduces batch mint and batch transfer features, allowing multiple tokens to be minted and/or transferred in a single on-chain transaction. This reduces the overall system load during periods of high activity, and users can claim their tokens faster and at a significantly lower cost. With the ability to batch 150 mints into a single transaction, users can claim their tokens faster and at a lower cost. - -![](/img/components/10.png) - -### Integer Range Type - -The Integer Range type is another new feature that makes creating Beams more efficient. By using this feature, you can specify a sequential range of token IDs for distribution using a shorthand format. So instead of specifying all of the token IDs you want to distribute individually, we can now simply provide an integer range in this format: "[startId..endId]" e.g. ["1..10000"] you can also mix and match single IDs and ranges: ["1", "5..10", "12", "15", "20..32"] - -![](/img/components/11.png) - -### Set attributes on the go! - -Developers can now set attributes to the minted tokens as they're being minted on-chain for the first time. This feature allows for unique and computed metadata to be added to tokens during the creation process. -This interaction with the mint on-demand feature is a match made in heaven as game developers will be able to create the tokens. At the same time, the beam is being generated, and they can set the metadata and mint the tokens as the demand for new tokens increases. - -![](/img/components/12.png) diff --git a/versioned_docs/version-2.x/06-enjin-products/_category_.json b/versioned_docs/version-2.x/06-enjin-products/_category_.json deleted file mode 100644 index 0291738..0000000 --- a/versioned_docs/version-2.x/06-enjin-products/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "Enjin Products", - "link": { - "type": "generated-index", - "description": "Delve into the core architecture and individual elements that constitute the Enjin ecosystem." - } -} - \ No newline at end of file diff --git a/versioned_sidebars/version-2.x-sidebars.json b/versioned_sidebars/version-2.x-sidebars.json deleted file mode 100644 index caea0c0..0000000 --- a/versioned_sidebars/version-2.x-sidebars.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "tutorialSidebar": [ - { - "type": "autogenerated", - "dirName": "." - } - ] -} diff --git a/versions.json b/versions.json deleted file mode 100644 index a61da5c..0000000 --- a/versions.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - "2.x" -] From 58ed09b641bfbec3e7b2483150382e01d8bb0653 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 1 Jun 2026 13:46:08 +0300 Subject: [PATCH 66/67] @ Fix v2 links on Tools page (faucet + platform URLs) - Replace the standalone v2 faucet (faucet.canary.enjin.io) with the built-in Canary faucet, linking to its docs section. - Point all Enjin Platform links at the v3 platform (platform.beta.enjin.io), replacing the v2 platform.canary.enjin.io and platform.enjin.io URLs. Co-Authored-By: Claude Opus 4.8 (1M context) @ --- src/pages/tools.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pages/tools.md b/src/pages/tools.md index c6a4e94..69fe031 100644 --- a/src/pages/tools.md +++ b/src/pages/tools.md @@ -9,17 +9,17 @@ To save time and money, we recommend building your initial proof-of-concept on C ### Building on Testnet 1. Download the [Enjin Wallet](https://enj.in/wallet). -2. Acquire some [cENJ](https://faucet.canary.enjin.io/) to test with. -3. Create an [Enjin Platform account](https://platform.canary.enjin.io). +2. Create an [Enjin Platform account](https://platform.beta.enjin.io). +3. Get some cENJ to test with from the [built-in Canary faucet](/getting-started/using-the-enjin-platform#canary-faucet) in the Platform UI. | Tool | Description | |------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [Enjin Platform Cloud](https://platform.canary.enjin.io) | The plug-and-play Web3-integration platform, enabling you to launch a full-scale valuable economy with ease. | -| [API Key & Settings](https://platform.canary.enjin.io/settings) | Enjin Platform settings where you can generate your API Key, authenticate your wallet account, and more. | -| [API Endpoint](https://platform.canary.enjin.io/graphql) | Use this to interact with the Enjin Platform API: `https://platform.canary.enjin.io/graphql` | -| [API Playground](https://platform.canary.enjin.io/graphiql) | An easy-to-use interface where you can run queries and mutations powered by Enjin’s GraphQL API. | +| [Enjin Platform Cloud](https://platform.beta.enjin.io) | The plug-and-play Web3-integration platform, enabling you to launch a full-scale valuable economy with ease. | +| [API Key & Settings](https://platform.beta.enjin.io/settings) | Enjin Platform settings where you can generate your API Key, authenticate your wallet account, and more. | +| [API Endpoint](https://platform.beta.enjin.io/graphql) | Use this to interact with the Enjin Platform API: `https://platform.beta.enjin.io/graphql` | +| [API Playground](https://platform.beta.enjin.io/graphiql) | An easy-to-use interface where you can run queries and mutations powered by Enjin’s GraphQL API. | | [Enjin Wallet](https://enj.in/wallet) | A powerful cross-chain wallet that allows you to sign transactions and manage your assets on both mainnet and testnet. | -| [cENJ Faucet](https://faucet.canary.enjin.io/) | Use this faucet to load testnet tokens (cENJ) into your account, enabling you to process unlimited transactions on the testnet for free. | +| [Built-in Canary Faucet](/getting-started/using-the-enjin-platform#canary-faucet) | The Platform's built-in faucet drops 250 cENJ to your Wallet Daemon's wallet — switch the network selector to Canary and click the faucet icon. Use it to process unlimited transactions on the testnet for free. | | [NFT.io Marketplace](https://canary.nft.io) | The Enjin Blockchain’s on-chain NFT marketplace that can be natively integrated into your games and apps | | [Subscan Block Explorer](https://canary-matrix.subscan.io/) | Allows you to extract, visualize, and review Canary data, including vital information about transactions, such as transaction history, wallet balances, fees, and other useful metrics. | | [Enjin Ecosystem Slack Community](https://enj.in/ecosystem-slack) | Join the official Enjin Ecosystem Slack community to connect with other developers and community members. | @@ -31,15 +31,15 @@ Once you have successfully integrated the [Canary Testnet](#canary-testnet-tools ### Launching on Mainnet 1. Build on [Testnet](#canary-testnet-tools) first. 2. Get some [ENJ](https://enjin.io/enjin-coin) to use. -3. Create an Enjin [Platform Cloud account](https://platform.enjin.io/), create a collection, and mint your tokens. +3. Create an Enjin [Platform Cloud account](https://platform.beta.enjin.io/), create a collection, and mint your tokens. 4. Transition from the to the endpoints. | Tool | Description | |---------------------------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Enjin Platform Cloud](https://platform.enjin.io) | The plug-and-play Web3-integration platform, enabling you to launch a full-scale valuable economy with ease. | -| [API Key & Settings](https://platform.enjin.io/settings) | Enjin Platform settings where you can generate your API Key, authenticate your wallet account, and more. | -| [API Endpoint](https://platform.enjin.io/graphql) | Use this to interact with the Enjin Platform API: `https://platform.enjin.io/graphql` | -| [API Playground](https://platform.enjin.io/graphiql) | An easy-to-use interface where you can run queries and mutations powered by Enjin’s GraphQL API. | +| [Enjin Platform Cloud](https://platform.beta.enjin.io) | The plug-and-play Web3-integration platform, enabling you to launch a full-scale valuable economy with ease. | +| [API Key & Settings](https://platform.beta.enjin.io/settings) | Enjin Platform settings where you can generate your API Key, authenticate your wallet account, and more. | +| [API Endpoint](https://platform.beta.enjin.io/graphql) | Use this to interact with the Enjin Platform API: `https://platform.beta.enjin.io/graphql` | +| [API Playground](https://platform.beta.enjin.io/graphiql) | An easy-to-use interface where you can run queries and mutations powered by Enjin’s GraphQL API. | | [Enjin Coin](https://enjin.io/enjin-coin) | Research Enjin Coin, the native currency that you can use to process transactions on the Enjin Blockchain. | | [NFT.io Marketplace](https://nft.io) | The Enjin Blockchain’s on-chain NFT marketplace that can be natively integrated into your games and apps | | [Subscan Block Explorer](https://matrix.subscan.io/) | Allows you to extract, visualize, and review Enjin Blockchain data, including vital information about transactions, such as transaction history, wallet balances, fees, and other useful metrics. | From 091f48a7839ac3c8eea185206d8d85c6fa197b5a Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 1 Jun 2026 15:30:04 +0300 Subject: [PATCH 67/67] @ Restructure Tools page for single v3 platform; drop "reads need no auth" note Tools page: the v2 page assumed two separate platforms (canary vs mainnet). v3 is one chain-agnostic dashboard with a network selector, so collapse the duplicated platform/API links into a single "Platform & API" section and keep only a small Canary-vs-mainnet table for the tools that genuinely differ (marketplace, explorer, native coin). Preserve the recommended testnet-first, then-mainnet flow. using-enjin-api: remove the note stating read-only queries are publicly accessible without an API token, since that access is temporary. Co-Authored-By: Claude Opus 4.8 (1M context) @ --- .../05-using-enjin-api/05-using-enjin-api.md | 4 -- src/pages/tools.md | 62 +++++++++---------- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/docs/01-getting-started/05-using-enjin-api/05-using-enjin-api.md b/docs/01-getting-started/05-using-enjin-api/05-using-enjin-api.md index c8e7f6d..a82407b 100644 --- a/docs/01-getting-started/05-using-enjin-api/05-using-enjin-api.md +++ b/docs/01-getting-started/05-using-enjin-api/05-using-enjin-api.md @@ -38,10 +38,6 @@ To make authenticated calls to the Enjin Platform: Authorization: Bearer ``` -:::note -Read-only queries (e.g. `GetAccount`, `GetToken`, `GetCollection`) are publicly accessible and do not require an API token. Mutations and account-scoped queries do. -::: - 4. _(Optional)_ To automate signing of transactions you create through the API, configure a Wallet Daemon with your API token. For more details head over to [Using the Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md). diff --git a/src/pages/tools.md b/src/pages/tools.md index 69fe031..03ae14a 100644 --- a/src/pages/tools.md +++ b/src/pages/tools.md @@ -1,16 +1,24 @@ import GlossaryTerm from '@site/src/components/GlossaryTerm'; -## Canary Testnet Tools +# Tools -To save time and money, we recommend building your initial proof-of-concept on Canary – the fast and free environment for testing Enjin's tools. +The Enjin Platform is **chain-agnostic** — a single dashboard and API endpoint serve both networks. Use the **network selector** in the top-right corner of the platform UI to switch between **Canary** (testnet) and **Enjin** (mainnet). -[Enjin Blockchain Mainnet](#enjin-blockchain-mainnet-tools) is , which means all transactions processed on it will exist on its ledger forever and attract gas fees and will exist on its ledger forever. +## Recommended Flow -### Building on Testnet + is — every transaction lives on its ledger forever and attracts real gas fees. So we recommend building and testing on the testnet first, then moving to mainnet: -1. Download the [Enjin Wallet](https://enj.in/wallet). -2. Create an [Enjin Platform account](https://platform.beta.enjin.io). -3. Get some cENJ to test with from the [built-in Canary faucet](/getting-started/using-the-enjin-platform#canary-faucet) in the Platform UI. +1. **Build and test on Canary** — the fast, free environment for testing Enjin's tools. + 1. Download the [Enjin Wallet](https://enj.in/wallet). + 2. Create an [Enjin Platform account](https://platform.beta.enjin.io). + 3. With the network selector set to **Canary**, get some cENJ to test with from the [built-in faucet](/getting-started/using-the-enjin-platform#canary-faucet). +2. **Move to Enjin mainnet** once your integration works end-to-end. + 1. Get some [ENJ](https://enjin.io/enjin-coin) to cover fees and storage deposits. + 2. Switch the network selector to **Enjin**, then create your collections and mint your tokens. + +## Platform & API + +These tools are the same on both networks — switch between Canary and Enjin using the network selector. | Tool | Description | |------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -19,28 +27,20 @@ To save time and money, we recommend building your initial proof-of-concept on C | [API Endpoint](https://platform.beta.enjin.io/graphql) | Use this to interact with the Enjin Platform API: `https://platform.beta.enjin.io/graphql` | | [API Playground](https://platform.beta.enjin.io/graphiql) | An easy-to-use interface where you can run queries and mutations powered by Enjin’s GraphQL API. | | [Enjin Wallet](https://enj.in/wallet) | A powerful cross-chain wallet that allows you to sign transactions and manage your assets on both mainnet and testnet. | -| [Built-in Canary Faucet](/getting-started/using-the-enjin-platform#canary-faucet) | The Platform's built-in faucet drops 250 cENJ to your Wallet Daemon's wallet — switch the network selector to Canary and click the faucet icon. Use it to process unlimited transactions on the testnet for free. | -| [NFT.io Marketplace](https://canary.nft.io) | The Enjin Blockchain’s on-chain NFT marketplace that can be natively integrated into your games and apps | -| [Subscan Block Explorer](https://canary-matrix.subscan.io/) | Allows you to extract, visualize, and review Canary data, including vital information about transactions, such as transaction history, wallet balances, fees, and other useful metrics. | +| [Built-in Canary Faucet](/getting-started/using-the-enjin-platform#canary-faucet) | The Platform's built-in faucet drops 250 cENJ to your Wallet Daemon's wallet — switch the network selector to Canary and click the faucet icon. | + +## Network-Specific Tools + +The marketplace, block explorer, and native coin differ between the two networks: + +| Tool | Canary (Testnet) | Enjin (Mainnet) | +|------------------------|---------------------------------------------------------------|----------------------------------------------| +| NFT.io Marketplace | [canary.nft.io](https://canary.nft.io) | [nft.io](https://nft.io) | +| Subscan Block Explorer | [canary-matrix.subscan.io](https://canary-matrix.subscan.io/) | [matrix.subscan.io](https://matrix.subscan.io/) | +| Native Coin | cENJ — free, from the [built-in faucet](/getting-started/using-the-enjin-platform#canary-faucet) | [ENJ](https://enjin.io/enjin-coin) | + +## Community + +| Tool | Description | +|------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------| | [Enjin Ecosystem Slack Community](https://enj.in/ecosystem-slack) | Join the official Enjin Ecosystem Slack community to connect with other developers and community members. | ---- - -## Enjin Blockchain Mainnet Tools -Once you have successfully integrated the [Canary Testnet](#canary-testnet-tools) into your game or app, you will be ready to launch on . - -### Launching on Mainnet -1. Build on [Testnet](#canary-testnet-tools) first. -2. Get some [ENJ](https://enjin.io/enjin-coin) to use. -3. Create an Enjin [Platform Cloud account](https://platform.beta.enjin.io/), create a collection, and mint your tokens. -4. Transition from the to the endpoints. - -| Tool | Description | -|---------------------------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Enjin Platform Cloud](https://platform.beta.enjin.io) | The plug-and-play Web3-integration platform, enabling you to launch a full-scale valuable economy with ease. | -| [API Key & Settings](https://platform.beta.enjin.io/settings) | Enjin Platform settings where you can generate your API Key, authenticate your wallet account, and more. | -| [API Endpoint](https://platform.beta.enjin.io/graphql) | Use this to interact with the Enjin Platform API: `https://platform.beta.enjin.io/graphql` | -| [API Playground](https://platform.beta.enjin.io/graphiql) | An easy-to-use interface where you can run queries and mutations powered by Enjin’s GraphQL API. | -| [Enjin Coin](https://enjin.io/enjin-coin) | Research Enjin Coin, the native currency that you can use to process transactions on the Enjin Blockchain. | -| [NFT.io Marketplace](https://nft.io) | The Enjin Blockchain’s on-chain NFT marketplace that can be natively integrated into your games and apps | -| [Subscan Block Explorer](https://matrix.subscan.io/) | Allows you to extract, visualize, and review Enjin Blockchain data, including vital information about transactions, such as transaction history, wallet balances, fees, and other useful metrics. | -| [Enjin Ecosystem Slack Community](https://enj.in/ecosystem-slack) | Join the official Enjin Ecosystem Slack community to connect with other developers and community members. | \ No newline at end of file