Enjin Farmer: Implementation Breakdown
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. 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 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.
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.
- Check for Collection: The server first checks if an
ENJIN_COLLECTION_ID
has been provided. - Create Collection: If the ID is missing, the server calls the
createCollection
function, which executes theCreateCollection
mutation on the Enjin Platform. The Wallet Daemon automatically signs the request.mutation CreateCollection($name: String!, ...) {
CreateCollection(...) {
id
method
state
}
} - Monitor Transaction: The mutation returns a request ID. The server then polls the
GetTransaction
query until the transactionstate
isFINALIZED
and theresult
isEXTRINSIC_SUCCESS
.query GetTransaction($requestId: Int!) {
GetTransaction(id: $requestId) {
state
result
events { ... }
}
} - Extract Collection ID: Once finalized, the server extracts the new collection ID from the transaction's events and assigns it to the
ENJIN_COLLECTION_ID
variable. - Create NFTs: Using a similar process, the server then creates the three resource NFTs (Gold Coin, Gold Coin (Blue), and Green Gem) within the collection by calling the
CreateToken
mutation for each and waiting for finalization.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, which verifies the player's identity before processing the request.
Authentication
GET /api/auth/health-check
: A simple endpoint to verify that the server is online.POST /api/auth/register
: Creates a new player and an associated managed wallet. To create the wallet, it calls theCreateWallet
mutation, using the player's email address as the uniqueexternalId
.mutation CreateWallet($externalId: String!) {
CreateWallet(externalId: $externalId)
}POST /api/auth/login
: Logs in an existing player, returning their wallet address and a JWT.
Wallet Management
POST /api/wallet/create
: Creates a new managed walletPOST /api/wallet/get
: Retrieves details for the authenticated player's managed wallet.GET /api/wallet/get-tokens
: Retrieves the player's managed wallet and all tokens it holds. It calls theGetWallet
query (using GraphQL Pagination to loop through all pages of results, ensuring the complete inventory is fetched).query GetWalletTokens($externalId: String!) {
GetWallet(externalId: $externalId) {
account { ... }
tokenAccounts(...) { ... }
}
}
Token Actions
POST /api/token/mint
: Mints a token to the player's wallet. The server calls theMintToken
mutation. The recipient address is extracted from the player's authenticated session.mutation mintToken($recipient: String!, $collectionId: BigInt!, ...) {
MintToken(recipient: $recipient, collectionId: $collectionId, ...) {
id
}
}POST /api/token/melt
: Melts a token from the player's wallet. This uses theBurn
mutation. 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 thesigningAccount
. (Learn more about managed wallets here)mutation burnToken($signingAccount: String!, $collectionId: BigInt!, ...) {
Burn(signingAccount: $signingAccount, collectionId: $collectionId, ...) {
id
}
}POST /api/token/transfer
: Transfers a token from the player's managed wallet to another address. This uses theSimpleTransferToken
mutation, again specifying the player's managed wallet as thesigningAccount
.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 theFarm_Outdoor
scene and configures the Host URL (e.g.,http://localhost:3000
) in the Inspector to connect to your game server.EnjinManager.cs
: A singleton controller that manages the player's session (auth token, wallet data) and exposes high-level methods likeMintToken()
for other game scripts to use.EnjinApiService.cs
: Handles all REST API communication with the game server using Unity'sUnityWebRequest
.EnjinItem.cs
: AScriptableObject
that represents the data of a blockchain item, such as its display name and its corresponding on-chain token ID.- UI Scripts (
BackpackUI.cs
,BackpackItemController.cs
): Scripts that manage the UI for viewing and interacting with the player's NFT inventory.
Initial Setup & Player Authentication
- Health Check: On launch, the client calls the
/api/auth/health-check
endpoint to ensure the server is available. - Login/Register: From the login screen, the player clicks "Login", which calls the
EnjinManager.Instance.RegisterAndLogin()
method. - API Request: This triggers
EnjinApiService
to send a POST request to the/api/auth/register
endpoint. - Store Auth Token: The server responds with a JWT authentication token. The client saves this token locally using
PlayerPrefs
and loads it on subsequent launches 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, they have a chance to find a resource token.
- An
EnjinToken
GameObject appears on the harvested tile. - When the player collects this GameObject, its
InteractedWith()
method is triggered. - This calls
EnjinItem.Collect()
, which in turn callsEnjinManager.Instance.MintToken()
. EnjinManager
then usesEnjinApiService
to send a request to the/api/token/mint
endpoint.
Viewing the Wallet (Backpack UI)
- Clicking the backpack icon opens the inventory screen, managed by
BackpackUI.cs
. - The UI calls
EnjinManager.Instance.GetManagedWalletTokens()
, which sends a request to the/api/wallet/get-tokens
endpoint. - The server saves the list of tokens, and the
BackpackUI
populates the view with the data. - The
BackpackUI
also subscribes to theEnjinManager.Instance.OnWalletUpdated
event to automatically refresh the inventory after a token is minted, melted, or transferred.
Melting and Transferring Tokens
- Melting: The player clicks "Melt" in the backpack. This flows through
EnjinManager
and sends a request to the/api/token/melt
endpoint. - Transferring: The player clicks "Send", sending a request to the
/api/token/transfer
endpoint.