For Cadence 0.42 go to Legacy Docs
Tutorial - How to Build FlowWords
Outline
The FlowWords tutorial example project is the complete FlowWords game, with the FlowSDK and any SDK related code removed.
In this tutorial, you will learn how to;
- Configure the FlowSDK and local emulator for use with Unity
- Deploy contracts, create Accounts and run Transactions on the Flow emulator from within Unity.
- Incorporate Flow Accounts, Transactions and Scripts into Unity projects using Code
Requirements
FlowWords Tutorial has been tested on Unity version 2021.3.6f1
This tutorial assumes you have created a blank project, and installed the FlowSDK package from the Unity Asset Store, and FlowWords Tutorial Sample.
Step 1 – Configure the FlowSDK and local emulator
- 
Open the scene at Assets\Samples\Flow SDK\<version>\Flow Words Tutorial Assets\Scenes\Game.unity  
- 
Open the Flow Control Manager window, at Window->Flow->Flow Control. 
 This will create a Flow Control GameObject in the Game scene, and a FlowControlData asset in the Resources folder if they do not already exist. 
- 
Go to the Emulator Settings tab. If the manager states the Flow Executable cannot be found, use the Install button provided and follow the instructions to install the Flow CLI. 
 Note: If Unity still does not detect the emulator after installing the Flow CLI, you may need to restart your computer for the install to take effect.
- 
Set a directory for our Emulator data to reside in. This is where the emulator stores the state of the emulated Flow blockchain. 
 For the purposes of this tutorial, we will create a new folder called FlowEmulator inside our project folder. 
- 
Click Start Emulator to start the emulator and create the emulator_service_account, and ensure “Run emulator in play mode” is checked.  NOTE: If this is the first time you have run the emulator on Windows, you may be presented with a dialog to allow access through your firewall. This is safe to allow. 
- 
Go to the Accounts tab and verify that the service account has been created. 
 This is the account we will use to create more accounts, and deploy our game contract on to the emulator. 
- 
Back on the Emulator Settings tab, you can click Show Emulator Log, to view the output.  
Step 2 – Deploy Contracts, create Accounts and run Transactions
We have provided you with the FlowWords game contract, but before we can interact with the contract, we have to deploy it to the blockchain.
We also have to set up some text replacements as, once deployed, our scripts will require hardcoded references to the contract name and deployed address.
Set up Text Replacements
Text replacements allow us to set and update references across all of our local Cadence scripts, without having to update each file individually.
- 
Open the Flow Control Manager window, and navigate to the Text Replacements tab. 
- 
Set up the text replacements as follows. You can add text replacements by clicking the ‘+’ button at the top of the panel.  The anatomy of a text replacement is as follows; 
 Description: A friendly description, has no bearing on functionality.
 Original Text: The original text, in your script files, which you want to replace.
 Replacement Text: The text that you wish to replace the original text with. This is what gets submitted to the chain.
 Active: This checkbox enables or disables the text replacement.
 Apply to Accounts: Select which, if any, accounts this text replacement should apply to.
 Apply to Gateways: Select which, if any, gateways (such as Emulator, or TestNet) this replacement should apply to.
Create User Accounts
While it would be perfectly possible to play our game with the emulator service account, we will often want to test our contracts with multiple different user accounts.
To create a new user account;
- 
Open the Flow Control Manager window, and navigate to the Tools tab. 
- 
In the Create New Account section;  - Select the paying account. This will usually be the emulator_service_account
- Enter a friendly name for the new account. This name is just for your own reference and does not get written to the chain.
- Click Create
 
- 
If successful, the new account will appear under the Flow Control Manager Accounts tab.  
Deploy the Contract
Before anyone can interact with a contract, it must be deployed to the blockchain.
We are going to deploy our game contract to the emulator, for local testing. But for deploying to Testnet, or Mainnet follow the same process.
- 
Go to the Emulator Settings tab, in the Flow Control Manager, and Start the Emulator if it is not already running. 
- 
Go to the Tools tab. In the Manage Contracts section, enter the contract name of ‘FlowWords’ – this should match our CONTRACT_NAME text replacement precisely. 
- 
Populate the Contract field with game-contract.cdc, which can be found in Resources/contracts  
- 
Finally, ensure that the Account field contains emulator_service_account, and click the Deploy Contract button.  
- 
To check if the contract has deployed successfully, open the Flow Emulator Output window. Successful deployment will look as follows;  
Submit Transactions
For administration purposes, it is sometimes useful to be able to directly submit transactions to the chain.
We can use a transaction to check out text replacements are set up correctly, and our game contract has successfully deployed.
- 
In Flow Control Manager, navigate to tools. 
- 
In the Transactions section;  - 
Populate the Transaction field with check-contract-deployed.cdc, located in Resources/transactions  
- 
Set the Signer field to the new Account we just created earlier. ‘Player1’ 
- 
Click Execute Transaction 
 
- 
- 
If you have successfully configured the SDK and deployed the game contract, you will see the following message;  
Step 3 – Incorporate Flow Accounts, Transactions and Scripts into Code
We have our SDK and emulator configured, and our game contract deployed.
Now we are going to create the code which will allow our game to send and receive data from the blockchain.
Our FlowWords tutorial project contains a script called FlowInterface.cs, which can be found in the Scripts folder. This script contains all of our blockchain interfacing functions, which are called from GameManager.cs and UIManager.cs.
Our game FlowInterface has 5 main functions:
- Login
- Logout
- GetGameDataFromChain
- SubmitGuess
- LoadHighScoresFromChain
Our functions are going to need to access a number of cadence scripts and transactions.
These have been provided for you, and can be found in the Resources\scripts folder and Resources\transactions folder.

FlowInterface.cs has a number of Serialized TextAsset fields, which can be populated via the Unity inspector. Select the GameFlowInterface gameobject in the Game.unity scene, and populate the fields as follows, using the scripts and transactions in the aforementioned folders; (you may find these have already been populated for you)

Login
Open FlowInterface.cs and find the Login function stub.
The Login function’s role is to take the credentials entered by the user, create a FlowControl.Account object with which we can submit transactions to the chain, and run the login.cdc transaction.
At the top of the file, add the following using statements to grant us easy access to the Flow SDK structures.
_10using DapperLabs.Flow.Sdk.Cadence;_10using DapperLabs.Flow.Sdk.DataObjects;_10using DapperLabs.Flow.Sdk.DevWallet;_10using Convert = DapperLabs.Flow.Sdk.Cadence.Convert;
Your file should now look like this:
_13using System;_13using System.Collections;_13using System.Collections.Generic;_13using System.Linq;_13using System.Numerics;_13using System.Threading.Tasks;_13using UnityEngine;_13using DapperLabs.Flow.Sdk;_13using DapperLabs.Flow.Sdk.Unity;_13using DapperLabs.Flow.Sdk.Cadence;_13using DapperLabs.Flow.Sdk.DataObjects;_13using DapperLabs.Flow.Sdk.DevWallet;_13using Convert = DapperLabs.Flow.Sdk.Cadence.Convert;
Uncomment the following at line 56:
_10//private FlowControl.Account FLOW_ACCOUNT = null;
We now have to register a wallet provider with the Flow SDK. We are going to use DevWallet, which comes with the Flow SDK, and is only intended for development purposes on emulator and testnet. Add the following to the Start function:
_10// Set up SDK to access Emulator_10FlowConfig flowConfig = new FlowConfig()_10{_10    NetworkUrl = "http://127.0.0.1:8888/v1",  // emulator_10    Protocol = FlowConfig.NetworkProtocol.HTTP_10};_10FlowSDK.Init(flowConfig);_10_10// Register DevWallet wallet provider with SDK_10FlowSDK.RegisterWalletProvider(new DevWalletProvider());
Your Start function should now look like this:
_18private void Start()_18{_18    if (Instance != this)_18    {_18        Destroy(this);_18    }_18_18    // Set up SDK to access Emulator_18    FlowConfig flowConfig = new FlowConfig()_18    {_18        NetworkUrl = "http://127.0.0.1:8888/v1",  // emulator_18        Protocol = FlowConfig.NetworkProtocol.HTTP_18    };_18    FlowSDK.Init(flowConfig);_18_18    // Register DevWallet wallet provider with SDK_18    FlowSDK.RegisterWalletProvider(new DevWalletProvider());_18}
WARNING: Do not use DevWallet in production builds. It is only intended for development purposes and does NOT securely store keys.
Having the Address and Private Keys to a blockchain account gives your application full access to all of that account’s funds and storage. They should be treated with extreme care.
Next, we will fill out the body of the Login function.
_11/// <summary>_11/// Attempts to log in by executing a transaction using the provided credentials_11/// </summary>_11/// <param name="username">An arbitrary username the player would like to be known by on the leaderboards</param>_11/// <param name="onSuccessCallback">Function that should be called when login is successful</param>_11/// <param name="onFailureCallback">Function that should be called when login fails</param>_11public void Login(string username, System.Action<string, string> onSuccessCallback, System.Action onFailureCallback)_11{_11    // Authenticate an account with DevWallet_11_11}
First, we have to invoke the wallet provider to authenticate the user and get their flow address. Add the following code to the Login function;
_10// Authenticate an account with DevWallet_10FlowSDK.GetWalletProvider().Authenticate(_10    "", // blank string will show list of accounts from Accounts tab of Flow Control Window_10    (string address) => onSuccessCallback(address, username), _10    onFailureCallback);
The Authenticate function takes parameters as follows;
- The first parameter is a username which corresponds to the name of an account in the Accounts tab. If you leave this string blank (as above), a dialog will be shown to the user to select an account from the Accounts tab.
- The second parameter is a success callback for Authenticate(). We pass in a lambda function which starts a coroutine to run our async functionOnAuthSuccess. This takes the flow address that we got fromAuthenticate(), as well as a few other parameters.
- The third parameter is a callback for if Authenticate()fails. We pass through the fail callback that was passed to Login.
Your completed Login function should look as follows;
_17/// <summary>_17/// Attempts to log in by executing a transaction using the provided credentials_17/// </summary>_17/// <param name="username">An arbitrary username the player would like to be known by on the leaderboards</param>_17/// <param name="onSuccessCallback">Function that should be called when login is successful</param>_17/// <param name="onFailureCallback">Function that should be called when login fails</param>_17public void Login(string username, System.Action<string, string> onSuccessCallback, System.Action onFailureCallback)_17{_17    // Authenticate an account with DevWallet_17    if (FlowSDK.GetWalletProvider().IsAuthenticated() == false)_17    {_17        FlowSDK.GetWalletProvider().Authenticate(_17            "", // blank string will show list of accounts from Accounts tab of Flow Control Window_17            (string address) => onSuccessCallback(address, username),_17            onFailureCallback);_17    }_17}
Now we need to implement the GetGameDataFromChain function for when we successfully authenticate our user and get their flow address.
At this point we have successfully authenticated our user, and all subsequent Scripts and Transactions will be submitted via the authenticated account.
GetGameDataFromChain
This function executes the get-current-gamestate.cdc transaction on the chain, and then processes the emitted events to get the CurrentGameState for the logged in account, and current GameStartTime for the game of the day, which we use to show time remaining.
_24/// <summary>_24/// Attempts to get the current game state for the user from chain._24/// </summary>_24/// <param name="username">An arbitrary username the player would like to be known by on the leaderboards</param>_24/// <param name="onSuccessCallback">Callback on success</param>_24/// <param name="onFailureCallback">Callback on failure</param>_24public IEnumerator GetGameDataFromChain(string username, System.Action<Decimal, List<GuessResult>, Dictionary<string, string>> onSuccessCallback, System.Action onFailureCallback)_24{_24    // get FLOW_ACCOUNT object for text replacements_24_24    // execute getCurrentGameState transaction on chain_24_24    // check for error. if so, break._24_24    // transaction success, get data from emitted events_24_24    // process currentGameState event_24_24    // process gameStartTime event_24_24    // call GameManager to set game state_24_24    yield return null;_24}
As you will recall, we previously configured a number of text replacements.
These are useful, and allow us to easily change some hardcoded data from a contract or transaction, such as the deploy address of the contract, from one central location without having to edit every cadence script in our project.
Our transactions and scripts will need to access the text replacement function, which lives in the FlowControl.Account class, so we need to first create a FlowControl.Account object as follows;
_10// get FLOW_ACCOUNT object for text replacements_10FLOW_ACCOUNT = new FlowControl.Account_10{_10    GatewayName = "Emulator",   // the network to match_10    AccountConfig = new Dictionary<string, string> { { "Address", FlowSDK.GetWalletProvider().GetAuthenticatedAccount().Address } } // the account address to match_10};
We are about to pull down the user's saved game data. To make processing event data easier, we have declared three classes to hold the results of events emitted by transactions:
_14public class StatePayload_14{_14    public List<GuessResult> currentState;_14}_14_14public class TimePayload_14{_14    public Decimal startTime;_14}_14_14public class GuessResultPayload_14{_14    public string result;_14}
Let's compare these to the payloads for each event in the contract:
_14pub event CurrentState(currentState: [UserGuess])_14pub event LastGameStart(startTime: UFix64)_14pub event GuessResult(result: String)_14_14pub struct UserGuess_14{_14    pub let Guess: String_14    pub let Result: String_14    init(guess: String, result: String)_14    {_14        self.Guess = guess_14        self.Result = result_14    }_14}
When using Cadence.Convert, cadence arrays are converted into C# Lists, as shown in the StatePayload class and CurrentState event.
In GameManager.cs, the GuessResult class is declared as:
_18public class GuessResult_18{_18    /// <summary>_18    /// the guess that was submitted_18    /// </summary>_18    [Cadence(CadenceType = "String", Name = "Guess")]_18    public string word;_18    /// <summary>_18    /// A 5 letter code indicating the result and resulting color a cell should be. _18    /// </summary>_18    /// <remarks>_18    /// "p" = the letter at this position was in the word and in the correct (p)osition, color the cell green._18    /// "w" = the letter at this position was in the (w)ord, but in the incorrect position, color the cell yellow._18    /// "n" = the letter at this position was (n)ot in the word._18    /// </remarks>_18    [Cadence(CadenceType = "String", Name = "Result")]_18    public string colorMap;_18}
We want the Cadence fields Guess and Result to map to the C# fields word and colorMap.  To do this, we add a Cadence attribute to each field with a Name
parameter that tells it which Cadence fields maps to that class field.
We did not have to do that with the three payload classes we defined earlier because the C# field names exactly match the Cadence field names in the contract.
Now that we have an Account object for text replacement, we can use it with the Transactions class to Submit our login.cdc transaction.
Add the following code to the GetGameDataFromChain function;
_11// execute getCurrentGameState transaction on chain_11Task<FlowTransactionResult> getStateTask = Transactions.SubmitAndWaitUntilExecuted(FLOW_ACCOUNT.DoTextReplacements(loginTxn.text), new CadenceString(username));_11_11while (!getStateTask.IsCompleted)_11{_11    int dots = ((int)(Time.time * 2.0f) % 4);_11_11    UIManager.Instance.SetStatus($"Retrieving data from chain" + new string('.', dots));_11_11    yield return null;_11}
Because transactions can take quite some time on chain, we create an asynchronous Task by calling SubmitAndWaitUntilExecuted from the Transactions class, to prevent blocking the main game thread.
Into SubmitAndWaitUntilExecuted, we pass the script that we want to execute, and any parameters.
For our script, we refer to the serialized TextAsset field, loginTxn, to which we will assign login.cdc in the inspector.
We pass our script into SubmitAndWaitUntilExecuted via the DoTextReplacements function on our FLOW_ACCOUNT object, which will parse the cadence script and replace any of our defined text replacements.
For parameters, the login.cdc script is expecting a single String parameter with the player’s display name in it. We pass in a new CadenceString object, which we create inline from the encapsulating function’s username string parameter.
Next, we simply wait until our asynchronous task.IsCompleted.
While we wait, we update the UI with a simple animated ‘Connecting…’ text status, and yield to the Unity engine to prevent blocking the thread.
Once our transaction has completed, we want to check if it was successful on chain. Add the following code beneath the transaction submission code;
_10// check for error. if so, break._10if (getStateTask.Result.Error != null || getStateTask.Result.ErrorMessage != string.Empty || getStateTask.Result.Status == FlowTransactionStatus.EXPIRED)_10{_10    onFailureCallback();_10    yield break;_10}
Here we must check the transaction Result for three conditions:
- Error: Was there an error submitting the transaction to the blockchain?
- ErrorMessage: Was there an error during processing on the blockchain?
- Status: A status of EXPIRED means that the transaction did not execute on time and was discarded.
Any error here, and we are simply going to fail the login, and call our onFailureCallback.
Next, we process the result of our transaction.
This transaction is designed to return the game state for the user, and the time remaining on the word of the day, via emitted events.
We can access these emitted events via the .Result.Events property on our task.
To do so, add the following code below our submission logic;
_10// transaction success, get data from emitted events_10List<FlowEvent> events = getStateTask.Result.Events;_10FlowEvent currentStateEvent = events.Find(x => x.Type.EndsWith(".CurrentState"));_10FlowEvent startTimeEvent = events.Find(x => x.Type.EndsWith(".LastGameStart"));_10_10if (currentStateEvent == null || startTimeEvent == null)_10{_10    onFailureCallback();_10    yield break;_10}
This chunk accesses the returned Events list, and attempts to find events ending with “.CurrentState” and “.LastGameStart”. We use .EndsWith, as the transaction returns fully qualified event names, and the name of the deployed contract may change during development.
Finally, we check that we do indeed have both of our required events, and if not, call the onFailure callback and break.
Next, we will parse the contents of each event. Add the following code to the function;
_36// process current game state event_36Decimal gameStartTime = 0;_36Dictionary<string, string> letterStatuses = new Dictionary<string, string>();_36List<GuessResult> results = Convert.FromCadence<StatePayload>(currentStateEvent.Payload).currentState;_36foreach (GuessResult newResult in results)_36{_36    newResult.word = newResult.word.ToUpper();_36    for (int i = 0; i < 5; i++)_36    {_36        bool letterAlreadyExists = letterStatuses.ContainsKey(newResult.word[i].ToString());_36        string currentStatus = letterAlreadyExists ? letterStatuses[newResult.word[i].ToString()] : "";_36        switch (currentStatus)_36        {_36            case "":_36                letterStatuses[newResult.word[i].ToString()] = newResult.colorMap[i].ToString();_36                break;_36            case "p":_36                break;_36            case "w":_36                if (newResult.colorMap[i] == 'p')_36                {_36                    letterStatuses[newResult.word[i].ToString()] = newResult.colorMap[i].ToString();_36                }_36                break;_36            case "n":_36                if (newResult.colorMap[i] == 'p' || newResult.colorMap[i] == 'w')_36                {_36                    letterStatuses[newResult.word[i].ToString()] = newResult.colorMap[i].ToString();_36                }_36                break;_36        }_36    }_36}_36_36// get game start time event_36gameStartTime = Convert.FromCadence<TimePayload>(startTimeEvent.Payload).startTime;
From the contract, we know that the CurrentState event returns a list of UserGuess structs.  We want to convert these to a C# List<GuessResult>.
_10List<GuessResult> results = Convert.FromCadence<StatePayload>(currentStateEvent.Payload).currentState;
This converts the Payload of the currentStateEvent event into a StatePayload object, then sets results to the currentState field of that object.
Then, we iterate over the results list and update the letterStatuses that we display.
The GameStartTime event is processed similarly:
_10gameStartTime = Convert.FromCadence<TimePayload>(startTimeEvent.Payload).startTime;
The startTimeEvent payload is converted into a TimePayload object and the startTime field is extracted from that. Because the Cadence type is UFix64, we get back a C# Decimal struct.
Finally, we call our onSuccess callback to return our results to our caller.
Add the following lines to the bottom of the function;
_10// call GameManager to set game state_10onSuccessCallback(gameStartTime, results, letterStatuses);
You can now remove the yield return null at the base of the function if you wish.
Your completed function should now look like this;
_85/// <summary>_85/// Attempts to get the current game state for the user from chain._85/// </summary>_85/// <param name="username">An arbitrary username the player would like to be known by on the leaderboards</param>_85/// <param name="onSuccessCallback">Callback on success</param>_85/// <param name="onFailureCallback">Callback on failure</param>_85public IEnumerator GetGameDataFromChain(string username, System.Action<Decimal, List<GuessResult>, Dictionary<string, string>> onSuccessCallback, System.Action onFailureCallback)_85{_85    // get FLOW_ACCOUNT object for text replacements_85    FLOW_ACCOUNT = new FlowControl.Account_85    {_85        GatewayName = "Emulator",   // the network to match_85        AccountConfig = new Dictionary<string, string> { { "Address", FlowSDK.GetWalletProvider().GetAuthenticatedAccount().Address } } // the account address to match_85    };_85_85    // execute getCurrentGameState transaction on chain_85    Task<FlowTransactionResult> getStateTask = Transactions.SubmitAndWaitUntilExecuted(FLOW_ACCOUNT.DoTextReplacements(loginTxn.text), new CadenceString(username));_85_85    while (!getStateTask.IsCompleted)_85    {_85        int dots = ((int)(Time.time * 2.0f) % 4);_85_85        UIManager.Instance.SetStatus($"Retrieving data from chain" + new string('.', dots));_85_85        yield return null;_85    }_85_85    // check for error. if so, break._85    if (getStateTask.Result.Error != null || getStateTask.Result.ErrorMessage != string.Empty || getStateTask.Result.Status == FlowTransactionStatus.EXPIRED)_85    {_85        onFailureCallback();_85        yield break;_85    }_85_85    // transaction success, get data from emitted events_85    List<FlowEvent> events = getStateTask.Result.Events;_85    FlowEvent currentStateEvent = events.Find(x => x.Type.EndsWith(".CurrentState"));_85    FlowEvent startTimeEvent = events.Find(x => x.Type.EndsWith(".LastGameStart"));_85_85    if (currentStateEvent == null || startTimeEvent == null)_85    {_85        onFailureCallback();_85        yield break;_85    }_85_85    // process current game state event_85    Decimal gameStartTime = 0;_85    Dictionary<string, string> letterStatuses = new Dictionary<string, string>();_85    List<GuessResult> results = Convert.FromCadence<StatePayload>(currentStateEvent.Payload).currentState;_85    foreach (GuessResult newResult in results)_85    {_85        newResult.word = newResult.word.ToUpper();_85        for (int i = 0; i < 5; i++)_85        {_85            bool letterAlreadyExists = letterStatuses.ContainsKey(newResult.word[i].ToString());_85            string currentStatus = letterAlreadyExists ? letterStatuses[newResult.word[i].ToString()] : "";_85            switch (currentStatus)_85            {_85                case "":_85                    letterStatuses[newResult.word[i].ToString()] = newResult.colorMap[i].ToString();_85                    break;_85                case "p":_85                    break;_85                case "w":_85                    if (newResult.colorMap[i] == 'p')_85                    {_85                        letterStatuses[newResult.word[i].ToString()] = newResult.colorMap[i].ToString();_85                    }_85                    break;_85                case "n":_85                    if (newResult.colorMap[i] == 'p' || newResult.colorMap[i] == 'w')_85                    {_85                        letterStatuses[newResult.word[i].ToString()] = newResult.colorMap[i].ToString();_85                    }_85                    break;_85            }_85        }_85    }_85_85    // get game start time event_85    gameStartTime = Convert.FromCadence<TimePayload>(startTimeEvent.Payload).startTime;_85_85    // call GameManager to set game state_85    onSuccessCallback(gameStartTime, results, letterStatuses);_85}
Logout
The Logout function’s role is to disconnect the authenticated wallet from your app, and to clear the FlowControl.Account object, to prevent any more transactions from being executed with those account credentials.
The Logout function is very simple. Simply add the following line;
_10FLOW_ACCOUNT = null;_10FlowSDK.GetWalletProvider().Unauthenticate();
This clears the FlowAccount object, preventing any more transactions from being submitted with it, and unauthenticates the user from the wallet provider.
Your completed Logout function should now look like this;
_10/// <summary>_10/// Clear the FLOW account object_10/// </summary>_10public void Logout()_10{_10    FLOW_ACCOUNT = null;_10    FlowSDK.GetWalletProvider().Unauthenticate();_10}
SubmitGuess
This function has two phases. First, it checks that the entered word is valid by submitting the check-word.cdc script to chain, and processing the returned value.
If the word is deemed valid, it then submits the word guess to the game contract using the currently logged in user’s credentials, by executing the submit-guess.cdc transaction script on chain, and then processing the emitted events.
For phase one, enter the following code at the top of the SubmitGuess function;
_23// submit word via checkWord script to FLOW chain to check if word is valid_23Task<FlowScriptResponse> checkWordTask = Scripts.ExecuteAtLatestBlock(FLOW_ACCOUNT.DoTextReplacements(checkWordScript.text), new CadenceString(word.ToLower()));_23_23while (!checkWordTask.IsCompleted)_23{_23    int dots = ((int)(Time.time * 2.0f) % 4);_23    UIManager.Instance.SetStatus("Waiting for server" + new string('.', dots));_23    yield return null;_23}_23_23if (checkWordTask.Result.Error != null)_23{_23    onFailureCallback();_23    UIManager.Instance.SetStatus("Error checking word validity.");_23    yield break;_23}_23_23bool wordValid = ((checkWordTask.Result.Value as CadenceString).Value == "OK");_23if (wordValid == false)_23{_23    onFailureCallback();_23    yield break;_23}
This code starts by calling ExecuteAtLatestBlock, passing in the checkWordScript and our guess word as a CadenceString object, to create an async Task using our Flow Account object.
Scripts on Cadence can be thought of as read-only transactions, which are performed very quickly.
Since scripts are read only, they do not require signing, and are best to use when you need to quickly get publicly available data from chain.
As with our previous transactions, we then wait until our task.IsCompleted, and then check for any errors in the result. With scripts we only have to check the Result.Error, as this catches all possible failure modes.
We then process the return value of the script, which can be found in the Result.Value property on our completed task object. Scripts do not emit events like transactions, but have return values like a regular function.
The return value is of the generic base type CadenceBase, which we cast to CadenceString, as we are expecting a string type return value.
If the word guess is deemed to be invalid we call the onFailure callback and break, otherwise we proceed onto the guess submission phase.
For the second phase of the function, add the following code below phase one;
_37// if word is valid, submit guess via transaction to FLOW chain_37Task<FlowTransactionResult> submitGuessTask = Transactions.SubmitAndWaitUntilExecuted(FLOW_ACCOUNT.DoTextReplacements(submitGuessTxn.text), new CadenceString(word.ToLower()));_37while (!submitGuessTask.IsCompleted)_37{_37    int dots = ((int)(Time.time * 2.0f) % 4);_37    UIManager.Instance.SetStatus("Waiting for server" + new string('.', dots));_37    yield return null;_37}_37_37if (submitGuessTask.Result.Error != null || submitGuessTask.Result.ErrorMessage != string.Empty || submitGuessTask.Result.Status == FlowTransactionStatus.EXPIRED)_37{_37    onFailureCallback();_37    yield break;_37}_37_37// get wordscore_37string wordScore = "";_37FlowEvent ourEvent = submitGuessTask.Result.Events.Find(x => x.Type.EndsWith(".GuessResult"));_37if (ourEvent != null)_37{_37    wordScore = Convert.FromCadence<GuessResultPayload>(ourEvent.Payload).result;_37_37    // check if we are out of guesses_37    if (wordScore == "OutOfGuesses")_37    {_37        onFailureCallback();_37        UIManager.Instance.SetStatus("Out Of Guesses. Try again tomorrow.");_37        yield break;_37    }_37_37    // process result_37    onSuccessCallback(word, wordScore);_37}_37else_37{_37    onFailureCallback();_37}
This phase begins by submitting the submit-guess.cdc transaction, passing in our guess word as a new CadenceString parameter. We then wait for the task to complete as usual, and check for any errors.
As this is a transaction, we once again check the three possible failure modes, and call the onFailure callback if the transaction failed.
Next we parse our transaction’s emitted events.
_10pub event GuessResult(result: String)
We are expecting an event called GuessResult, with a single string parameter called result.  We created a C# version of that event: GuessResultPayload.
_22// get wordscore_22string wordScore = "";_22FlowEvent ourEvent = submitGuessTask.Result.Events.Find(x => x.Type.EndsWith(".GuessResult"));_22if (ourEvent != null)_22{_22    wordScore = Convert.FromCadence<GuessResultPayload>(ourEvent.Payload).result;_22_22    // check if we are out of guesses_22    if (wordScore == "OutOfGuesses")_22    {_22        onFailureCallback();_22        UIManager.Instance.SetStatus("Out Of Guesses. Try again tomorrow.");_22        yield break;_22    }_22_22    // process result_22    onSuccessCallback(word, wordScore);_22}_22else_22{_22    onFailureCallback();_22}
We first find our event in the Result.Events list on our task object.
If our event is found, we then convert the payload to a GuessResultPayload and store the result field as wordScore.  We then pass the guess word, and the result back to our caller via the onSuccess callback.
If the GuessResult event cannot be found in the Result.Events list, we call the onFailure callback.
Once complete, your SubmitGuess function should look like this:
_63public IEnumerator SubmitGuess(string word, System.Action<string, string> onSuccessCallback, System.Action onFailureCallback)_63{_63    // submit word via checkWord script to FLOW chain to check if word is valid_63    Task<FlowScriptResponse> checkWordTask = Scripts.ExecuteAtLatestBlock(FLOW_ACCOUNT.DoTextReplacements(checkWordScript.text), new CadenceString(word.ToLower()));_63    while (!checkWordTask.IsCompleted)_63    {_63        int dots = ((int)(Time.time * 2.0f) % 4);_63        UIManager.Instance.SetStatus("Waiting for server" + new string('.', dots));_63        yield return null;_63    }_63_63    if (checkWordTask.Result.Error != null)_63    {_63        onFailureCallback();_63        UIManager.Instance.SetStatus("Error checking word validity.");_63        yield break;_63    }_63_63    bool wordValid = ((checkWordTask.Result.Value as CadenceString).Value == "OK");_63    if (wordValid == false)_63    {_63        onFailureCallback();_63        yield break;_63    }_63    _63    // if word is valid, submit guess via transaction to FLOW chain_63    Task<FlowTransactionResult> submitGuessTask = Transactions.SubmitAndWaitUntilExecuted(FLOW_ACCOUNT.DoTextReplacements(submitGuessTxn.text), new CadenceString(word.ToLower()));_63    while (!submitGuessTask.IsCompleted)_63    {_63        int dots = ((int)(Time.time * 2.0f) % 4);_63        UIManager.Instance.SetStatus("Waiting for server" + new string('.', dots));_63        yield return null;_63    }_63_63    if (submitGuessTask.Result.Error != null || submitGuessTask.Result.ErrorMessage != string.Empty || submitGuessTask.Result.Status == FlowTransactionStatus.EXPIRED)_63    {_63        onFailureCallback();_63        yield break;_63    }_63_63    // get wordscore_63    string wordScore = "";_63    FlowEvent ourEvent = submitGuessTask.Result.Events.Find(x => x.Type.EndsWith(".GuessResult"));_63    if (ourEvent != null)_63    {_63        wordScore = Convert.FromCadence<GuessResultPayload>(ourEvent.Payload).result;_63_63        // check if we are out of guesses_63        if (wordScore == "OutOfGuesses")_63        {_63            onFailureCallback();_63            UIManager.Instance.SetStatus("Out Of Guesses. Try again tomorrow.");_63            yield break;_63        }_63_63        // process result_63        onSuccessCallback(word, wordScore);_63    }_63    else_63    {_63        onFailureCallback();_63    }_63}
LoadHighScoresFromChain
This function fires off a number of small scripts simultaneously, which pull publicly available high score data from the game contract on chain using;
- get-highscores.cdc
- get-player-cumulativescore.cdc
- get-player-guess-distribution.cdc
- get-player-maxstreak.cdc
- get-player-scores.cdc
- get-player-streak.cdc.
It then processes their returned values, and passes them out to the onSuccess call for the high scores UI to render.
For this function, we are going to first fire off a number of simultaneous scripts on the blockchain. This is something you want to avoid with transactions, as transaction order of execution cannot be guaranteed due to the distributed nature of blockchain, however as scripts are read-only, and do not mutate the chain, order of execution is far less likely to matter.
To execute the scripts, add the following code to the top of the function;
_32// get player's wallet public address_32string playerWalletAddress = FlowSDK.GetWalletProvider().GetAuthenticatedAccount().Address;_32_32// execute scripts to get highscore data_32Dictionary<string, Task<FlowScriptResponse>> tasks = new Dictionary<string, Task<FlowScriptResponse>>();_32tasks.Add("GetHighScores", Scripts.ExecuteAtLatestBlock(FLOW_ACCOUNT.DoTextReplacements(GetHighScores.text)));_32tasks.Add("GetPlayerCumulativeScore", Scripts.ExecuteAtLatestBlock(FLOW_ACCOUNT.DoTextReplacements(GetPlayerCumulativeScore.text), new CadenceAddress(playerWalletAddress)));_32tasks.Add("GetPlayerWinningStreak", Scripts.ExecuteAtLatestBlock(FLOW_ACCOUNT.DoTextReplacements(GetPlayerWinningStreak.text), new CadenceAddress(playerWalletAddress)));_32tasks.Add("GetPlayerMaxWinningStreak", Scripts.ExecuteAtLatestBlock(FLOW_ACCOUNT.DoTextReplacements(GetPlayerMaxWinningStreak.text), new CadenceAddress(playerWalletAddress)));_32tasks.Add("GetGuessDistribution", Scripts.ExecuteAtLatestBlock(FLOW_ACCOUNT.DoTextReplacements(GetGuessDistribution.text), new CadenceAddress(playerWalletAddress)));_32_32// wait for completion_32bool complete = false;_32while (!complete)_32{_32    complete = true;_32    foreach (KeyValuePair<string, Task<FlowScriptResponse>> task in tasks)_32    {_32        complete = complete && task.Value.IsCompleted;_32    }_32    yield return null;_32}_32_32// check for errors_32foreach (KeyValuePair<string, Task<FlowScriptResponse>> task in tasks)_32{_32    if (task.Value.Result.Error != null)_32    {_32        onFailureCallback();_32        yield break;_32    }_32}
This block of code first obtains the public address of the current authenticated player and creates a Dictionary<string, Task> to store our concurrent script tasks.
We then sequentially create async Tasks for each script that we want to execute, using ExecuteAtLatestBlock, and add them to the Task dictionary.
In our use case, we want all of the tasks to complete before we display any results, so our wait for completion code block iterates over every Task in the dictionary, and only moves on once every task.IsComplete.
Checking for errors is similarly done using a foreach loop, where every task is checked to ensure the Error field is null. If even one task has an Error, we call the onFailure callback and break.
Next we need to process the returned values. Add the following code beneath the previous;
_12// load global highscores_12List<ScoreStruct> GlobalScores = Convert.FromCadence<List<ScoreStruct>>(tasks["GetHighScores"].Result.Value);_12GlobalScores = GlobalScores.OrderByDescending(score => score.Score).Take(10).ToList();_12_12// load player scores_12BigInteger PlayerCumulativeScore = Convert.FromCadence<BigInteger>(tasks["GetPlayerCumulativeScore"].Result.Value);_12BigInteger PlayerWinningStreak = Convert.FromCadence<BigInteger>(tasks["GetPlayerWinningStreak"].Result.Value);_12BigInteger PlayerMaximumWinningStreak = Convert.FromCadence<BigInteger>(tasks["GetPlayerMaxWinningStreak"].Result.Value);_12List<BigInteger> PlayerGuessDistribution = Convert.FromCadence<List<BigInteger>>(tasks["GetGuessDistribution"].Result.Value);_12_12// callback_12onSuccessCallback(GlobalScores, PlayerCumulativeScore, PlayerWinningStreak, PlayerMaximumWinningStreak, PlayerGuessDistribution);
Our global highscores are an array of Scores objects in the contract.
_10access(contract) let TopScores : [Scores]_10pub struct Scores_10{_10    pub let AccId : Address_10    pub let Name : String_10    pub let Score : UInt_10}
We have a ScoreStruct defined HighScoresPanel.cs as:
_10public struct ScoreStruct_10{_10    public string Name;_10    public BigInteger Score;_10}
_10List<ScoreStruct> GlobalScores = Convert.FromCadence<List<ScoreStruct>>(tasks["GetHighScores"].Result.Value);_10GlobalScores = GlobalScores.OrderByDescending(score => score.Score).Take(10).ToList();
Here we get the result of the GetHighScores task and convert it into a List<ScoreStruct>.  Then we reorder the list and keep only the highest ten values.
Next, we parse the detailed statistics for the current player, using Convert.FromCadence to convert from the Cadence values into the C# types we want.
Finally, we call the onSuccess callback, passing in all of our parsed results.
Once complete, your function should look as follows;
_50public IEnumerator LoadHighScoresFromChain(System.Action<List<ScoreStruct>, BigInteger, BigInteger, BigInteger, List<BigInteger>> onSuccessCallback, System.Action onFailureCallback)_50{_50    // get player's wallet public address_50    string playerWalletAddress = FlowSDK.GetWalletProvider().GetAuthenticatedAccount().Address;_50_50    // execute scripts to get highscore data_50    Dictionary<string, Task<FlowScriptResponse>> tasks = new Dictionary<string, Task<FlowScriptResponse>>();_50    tasks.Add("GetHighScores", Scripts.ExecuteAtLatestBlock(FLOW_ACCOUNT.DoTextReplacements(GetHighScores.text)));_50    tasks.Add("GetPlayerCumulativeScore", Scripts.ExecuteAtLatestBlock(FLOW_ACCOUNT.DoTextReplacements(GetPlayerCumulativeScore.text), new CadenceAddress(playerWalletAddress)));_50    tasks.Add("GetPlayerWinningStreak", Scripts.ExecuteAtLatestBlock(FLOW_ACCOUNT.DoTextReplacements(GetPlayerWinningStreak.text), new CadenceAddress(playerWalletAddress)));_50    tasks.Add("GetPlayerMaxWinningStreak", Scripts.ExecuteAtLatestBlock(FLOW_ACCOUNT.DoTextReplacements(GetPlayerMaxWinningStreak.text), new CadenceAddress(playerWalletAddress)));_50    tasks.Add("GetGuessDistribution", Scripts.ExecuteAtLatestBlock(FLOW_ACCOUNT.DoTextReplacements(GetGuessDistribution.text), new CadenceAddress(playerWalletAddress)));_50_50    // wait for completion_50    bool complete = false;_50    while (!complete)_50    {_50        complete = true;_50        foreach (KeyValuePair<string, Task<FlowScriptResponse>> task in tasks)_50        {_50            complete = complete && task.Value.IsCompleted;_50        }_50        yield return null;_50    }_50_50    // check for errors_50    foreach (KeyValuePair<string, Task<FlowScriptResponse>> task in tasks)_50    {_50        if (task.Value.Result.Error != null)_50        {_50            onFailureCallback();_50            yield break;_50        }_50    }_50_50    // load global highscores_50    List<ScoreStruct> GlobalScores = Convert.FromCadence<List<ScoreStruct>>(tasks["GetHighScores"].Result.Value);_50    GlobalScores = GlobalScores.OrderByDescending(score => score.Score).Take(10).ToList();_50_50    // load player scores_50    BigInteger PlayerCumulativeScore = Convert.FromCadence<BigInteger>(tasks["GetPlayerCumulativeScore"].Result.Value);_50    BigInteger PlayerWinningStreak = Convert.FromCadence<BigInteger>(tasks["GetPlayerWinningStreak"].Result.Value);_50    BigInteger PlayerMaximumWinningStreak = Convert.FromCadence<BigInteger>(tasks["GetPlayerMaxWinningStreak"].Result.Value);_50    List<BigInteger> PlayerGuessDistribution = Convert.FromCadence<List<BigInteger>>(tasks["GetGuessDistribution"].Result.Value);_50_50    // callback_50    onSuccessCallback(GlobalScores, PlayerCumulativeScore, PlayerWinningStreak, PlayerMaximumWinningStreak, PlayerGuessDistribution);_50_50    yield return null;_50}
Step 5 – Play FlowWords!
If you have correctly followed all of the steps above, you will now have a working project.
- Return to the Unity editor, and press the Play button.
- Enter a friendly name - this will appear on the leaderboard.
- Click Log In.
- Select an account from the dialog that appears to authenticate with.
You should see the login screen, say Connecting…, and then Loading…
Followed shortly thereafter by the game screen;

And the High Scores screen (if you click the button);

Step 6 – Further Experimentation
For an extra challenge, try some of the following;
- Add more accounts and play with some friends, hot seat style
- Modify the game-contract.cdc to make a new game every 5 minutes instead of every 24 hours.
- Try to remove and redeploy the contract
 (hint: on testnet and mainnet, once removed, a contract’s name can never be used again on the same account)
 (extra hint: delete-game-resources.cdc)
- Poke about in the game contracts, scripts and transactions to see what they do!
If you ever get the emulator into a messy state, you can always hit the Clear Persistent Data button, which will wipe the emulator back to its blank state. This will of course lose all deployed contracts and high score and game history.
Appendix – How to convert FlowWords to run on TestNet
To modify the tutorial project to run on TestNet, only minor modifications are required.
- Change the network configuration to point to TestNet
- Replace the DevWallet provider with a more secure solution. e.g. WalletConnect
- Configure Account and update Text Replacements
- Deploy the contract to TestNet
Change the network configuration to TestNet
To change the network configuration, simply modify the start function as follows;
_18private void Start()_18{_18    if (Instance != this)_18    {_18        Destroy(this);_18    }_18_18    // Set up SDK to access TestNet_18    FlowConfig flowConfig = new FlowConfig()_18    {_18        NetworkUrl = "https://rest-testnet.onflow.org/v1",  // testnet_18        Protocol = FlowConfig.NetworkProtocol.HTTP_18    };_18    FlowSDK.Init(flowConfig);_18_18    // Register DevWallet wallet provider with SDK_18    FlowSDK.RegisterWalletProvider(new DevWalletProvider());_18}
We have now replaced the emulator address with the address for the TestNet access point, and all subsequent transactions and scripts will be directed to TestNet.
Replace DevWallet with WalletConnect
To now change the wallet provider, simply modify the start function as follows;
_29private void Start()_29{_29    if (Instance != this)_29    {_29        Destroy(this);_29    }_29_29    // Set up SDK to access TestNet_29    FlowConfig flowConfig = new FlowConfig()_29    {_29        NetworkUrl = "https://rest-testnet.onflow.org/v1",  // testnet_29        Protocol = FlowConfig.NetworkProtocol.HTTP_29    };_29    FlowSDK.Init(flowConfig);_29_29    // Create WalletConnect wallet provider_29    IWallet walletProvider = new WalletConnectProvider();_29    walletProvider.Init(new WalletConnectConfig_29    {_29        ProjectId = "<YOUR PROJECT ID>", // insert Project ID from Wallet Connect dashboard_29        ProjectDescription = "A simple word guessing game built on FLOW!",_29        ProjectIconUrl = "https://walletconnect.com/meta/favicon.ico",_29        ProjectName = "FlowWords",_29        ProjectUrl = "https://dapperlabs.com"_29    });_29_29    // Register WalletConnect wallet provider with SDK_29    FlowSDK.RegisterWalletProvider(walletProvider);_29}
You will also need to add the following using declarations to the top of the file;
_10using DapperLabs.Flow.Sdk.WalletConnect;_10using DapperLabs.Flow.Sdk.Crypto;
For this modification we have created a new WalletConnectProvider, and initialized it, and then registered our new WalletConnectProvider.
The only thing missing is a Project Id.
Each WalletConnect application requires its own project id. You can get one by going to https://cloud.walletconnect.com and signing up for an account.
You can then create a new project on the website, give it a name (we suggest FlowWords), and enter the project id provided into your code.
Finally, we need one more change to make sure our text replacement still functions correctly.
At the beginning of the OnAuthSuccess function, change the FLOW_ACCOUNT GatewayName from "Emulator" to "Flow Testnet".
_10// get FLOW account - we are only going to use this for text replacements_10FLOW_ACCOUNT = new FlowControl.Account_10{_10    GatewayName = "Flow Testnet",_10    AccountConfig = new Dictionary<string, string> { { "Address", FlowSDK.GetWalletProvider().GetAuthenticatedAccount().Address } }_10};
Configure Account and update Text Replacements
To deploy the contract to TestNet, the only current way to do so via the FlowSDK, is to use a TestNet account to which you know the private key.
First, in the Accounts tab of the Flow Control window, create a new account by clicking the plus '+' icon at the top of the window.
Create the new account as follows, replacing the Address and Private Key with your own valid TestNet address / private key pair.

Switch to the Text Replacements tab of the Flow Control window now, and create a new text replacement definition as follows, replacing the given replacement text with the address to your TestNet account.

Now modify the existing CONTRACT_NAME text replacement, by changing the Apply to Gateways field from Emulator, to All. This will make sure this replacement also applies to TestNet transactions.
Deploy the Contract to TestNet
Now that all of the configuration is done, you can deploy the contract.
On the Tools tab of the Flow Control window, set up the Manage Contracts section as follows;

Where;
- Contract Name is the name of the contract on the blockchain. This should match the text replacement configured for CONTRACT_NAME
- Contract is the game-contract.cdc asset included with the project sample in the Assets\Samples\Flow SDK\<version>\Flow Words Tutorial Assets\Resources directory.
- Account is the new Deploy Account that you created in the previous step.
Once you have the Manage Contracts section filled in as per the example, click Deploy Contract.
If all goes well, you will see the text 'Executing Transaction' appear below the Deploy Contract button, followed by a clickable link to the transaction result on Flowdiver.
Congratulations! You can now run FlowWords, and play the game with a WalletConnect compatible wallet!
NOTE: TestNet requires that all contracts deployed to an account have a unique name, does not allow contract removal without authorisation, and only allows contract updates that do not break interfaces or data structures or introduce undefined data to existing resources.
Due to these limitations, iterative development should always be done on Emulator before attempting to push anything to a live network.