FlowControl scripting example
This example shows how to use FlowControl to perform common tasks purely from C#, without using the FlowControl editor interface. This will ignore any of the GUI related code and focus on using the FlowControl and Flow SDK.
Prerequisites
Ensure you have flow-cli installed. This will allow us to use an emulated flow environment. You can install it by following the instructions at Flow CLI
Sample walk through
You can follow along in FlowControlExample.cs
The first thing to notice is that we declare Start() to be an IEnumerator. This makes Start a coroutine. You will always want to run FlowSDK functions inside a coroutine because they can take a while to complete and you don't want to lock up your game while they are processed.
_10private IEnumerator Start()_10{_10 // Your start code_10}
Checking emulator state
The next thing we do is ensure the emulator is running. We give it a few seconds to start:
_14//Wait up to 2.5 seconds for the emulator to start._14int waited = 0;_14_14while (!FlowControl.IsEmulatorRunning && waited < 5)_14{_14 waited++;_14 yield return new WaitForSeconds(.5f);_14}_14_14if (!FlowControl.IsEmulatorRunning)_14{_14 //Stop execution if the emulator is not running by now._14 yield break;_14}
Creating a FlowControl Account
Next we'll create a FlowControl account to use ONLY for running scripts. The Flow network doesn't require an account to run scripts, but FlowControl uses Accounts to determine which network to connect to.
_10FlowControl.Account scriptOnlyAccount = new FlowControl.Account_10{_10 GatewayName = "Emulator"_10};
Because this account doesn't have any keys associated with it, it can't be used to run transactions. It does define which Gateway to use, in this case the "Emulator" gateway, so it can be used to run scripts.
Running scripts
Next, we'll use this account to run a script on the emulator. Scripts on Flow are written in Cadence. More information is available at Developer Portal
First we'll define the script that we want to run:
_10const string code = @"pub fun main(message: String): Int{_10 log(message)_10 return 42_10}";
This script requires a Cadence String as input, returns a Cadence Int, and will log the input string to the emulator log.
Now we execute this script:
_10Task<FlowScriptResponse> task = scriptOnlyAccount.ExecuteScript(code, Convert.ToCadence("Test", "String"));
FlowControl uses an Account oriented approach. Everything is done using an Account object. In this case we'll use the scriptOnlyAccount account that we created earlier to call ExecuteScript.
A script is code that can not permanently mutate the state of the blockchain. It is read-only. It CAN call functions that would change the state of the blockchain, but any changes that are made will be discarded once the script finishes running.
We pass in the Cadence code we want to run and any arguments that are required by the script. We need to use Cadence specific data types, so we construct a new CadenceString using the string "Test".
This returns a Task<FlowScriptResponse>
. This is an asynchronous Task that will result
in a FlowScriptResponse when it is complete.
Next, we need to wait for the Task to complete. Inside a Unity coroutine we can use the WaitUntil
function as follows:
_10yield return new WaitUntil(() => task.IsCompleted);
WaitUntil
takes a function that returns a bool (Func<bool>
), so we construct an anonymous one that
returns the IsCompleted field of the task. This cause Unity to pause execution of the current coroutine
until the task is completed.
We then check to see if an error occured, and if so, log it to the console.
_10if (task.Result.Error != null)_10{_10 Debug.LogError($"Error: {task.Result.Error.Message}");_10 yield break;_10}
If there is no error, the script should have returned a Cadence Int value. We can access it as follows:
_10Debug.Log($"Script result: {Convert.FromCadence<BigInteger>(task.Result.Value)}");
This might be a bit confusing. The Task will have a Result. The result could contain an error, but we checked for that earlier. If it doesn't contain an error, then it will contain a Value.
That Value will be of type CadenceBase, which is the base type for all Cadence data types. We know that the script returns a number, so we can convert it to an appropriate data type using Convert.FromCadence.
Creating an SdkAccount
Next, let's create an account that can be used to execute transactions that mutate the state of the blockchain. This will also demonstrate how you can use both FlowControl and the base SDK together.
_10SdkAccount emulatorSdkAccount = FlowControl.GetSdkAccountByName("emulator_service_account");_10if (emulatorSdkAccount == null)_10{_10 Debug.LogError("Error getting SdkAccount for emulator_service_account");_10 yield break;_10}
When the emulator is started, FlowControl automatically creates an emulator_service_account FlowControl.Account for you to use to access the built in emulator service account. We'll use that account to create a new account.
Because the CreateAccount
function is an SDK function, and not a FlowControl function, we'll need to create a
temporary SdkAccount
from the FlowControl Account. The GetSdkAccountByName
function will construct an
SdkAccount object from a FlowControl.Account object.
If the name you pass to FlowControl.GetSdkAccountByName
does not exist, it will return null, so we check
for that and stop execution if it fails.
Creating an account on the blockchain
Now we'll use this new SdkAccount object to create a new Flow account on the emulated blockchain.
_32FlowSDK.RegisterWalletProvider(ScriptableObject.CreateInstance<DevWalletProvider>());_32_32string authAddress = "";_32FlowSDK.GetWalletProvider().Authenticate("", (string address) =>_32{_32 authAddress = address;_32}, null);_32_32yield return new WaitUntil(() => { return authAddress != ""; });_32_32//Convert FlowAccount to SdkAccount_32SdkAccount emulatorSdkAccount = FlowControl.GetSdkAccountByAddress(authAddress);_32if (emulatorSdkAccount == null)_32{_32 Debug.LogError("Error getting SdkAccount for emulator_service_account");_32 yield break;_32}_32_32//Create a new account with the name "User"_32Task<SdkAccount> newAccountTask = CommonTransactions.CreateAccount("User");_32yield return new WaitUntil(() => newAccountTask.IsCompleted);_32_32if (newAccountTask.Result.Error != null)_32{_32 Debug.LogError($"Error creating new account: {newAccountTask.Result.Error.Message}");_32 yield break;_32}_32_32outputText.text += "DONE\n\n";_32_32//Here we have an SdkAccount_32SdkAccount userSdkAccount = newAccountTask.Result;
First we create and register a new DevWalletProvider
. Any time a transaction is run, it calls the provided wallet provider. The DevWalletProvider
is an implementation of IWallet that shows a simulated wallet interface. It will allow you to view and authorize the submitted transaction.
After creating and registering the wallet provider, we call Authenticate
to display a popup that will allow you to select any of the accounts in the FlowControl
Accounts tab. You should choose emulator_service_account when prompted when running the demo.
We then wait until the user has selected an account.
CommonTransactions
contains some utility functions to make performing frequent operations a little easier.
One of these is CreateAccount
. It expects a Name
, which is not placed on the blockchain, and the SdkAccount
that should pay for the creation of the new account. That returns a Task that is handled similarly to
before.
If there is no error, the Result field of the task will contain the newly create account info.
Now, in order to use this new account with FlowControl, we'll need to create a FlowControl.Account from the SdkAccount we have.
_10FlowControl.Account userAccount = new FlowControl.Account_10{_10 Name = userSdkAccount.Name,_10 GatewayName = "Emulator",_10 AccountConfig = new Dictionary<string, string>_10 {_10 ["Address"] = userSdkAccount.Address,_10 ["Private Key"] = userSdkAccount.PrivateKey_10 }_10};
Then we store this account in the FlowControlData object so that we can look it up by name later.
_10FlowControl.Data.Accounts.Add(userAccount);
Deploying a contract
The next section shows how to deploy a contract to the Flow network. Because this is another utility
function from CommonTransactions
, it needs an SdkAccount. We'll use userSdkAccount we created earlier.
First we need to define the contract we wish to deploy.
_15const string contractCode = @"_15 pub contract HelloWorld {_15 pub let greeting: String_15_15 pub event TestEvent(field: String)_15_15 init() {_15 self.greeting = ""Hello, World!""_15 }_15_15 pub fun hello(data: String): String {_15 emit TestEvent(field:data)_15 return self.greeting_15 }_15 }";
We won't discuss how to write Flow contracts in depth here, but simply put this contract defines a single function that will emit an event and return the string "Hello World!" when run.
Then we use the same pattern we've used before to deploy this contract using the CommonTransaction.DeployContract
function. Note that we should register a new wallet provider since we are changing the account we want to run the transaction
as.
_11FlowSDK.GetWalletProvider().Authenticate(userAccount.Name, null, null);_11Task<FlowTransactionResponse> deployContractTask = _11 CommonTransactions.DeployContract("HelloWorld", contractCode);_11_11yield return new WaitUntil(() => deployContractTask.IsCompleted);_11_11if (deployContractTask.Result.Error != null)_11{_11 Debug.LogError($"Error deploying contract: {deployContractTask.Result.Error.Message}");_11 yield break;_11}
We'll reauthenticate with the wallet provider to tell it to use the new newly created account. Because we pass in a name this time, it won't display the select account pop-up.
The first argument to DeployContract
is the contract name. This must match the name in the contract
data itself. The second argument is the Cadence code that defines the contract, and the third argument
is the SdkAccount that the contract should be deployed to.
Replacement text
Next we'll see how to add a ReplacementText
entry to FlowControl. This is typically done via the
FlowControl editor interface, but can be done programatically as shown.
_11FlowControl.TextReplacement newTextReplacement = new FlowControl.TextReplacement_11{_11 description = "User Address",_11 originalText = "%USERADDRESS%",_11 replacementText = userSdkAccount.Address,_11 active = true,_11 ApplyToAccounts = new List<string> { "User" },_11 ApplyToGateways = new List<string> { "Emulator" }_11};_11_11FlowControl.Data.TextReplacements.Add(newTextReplacement);
Note that we are setting ApplyToAccounts
and ApplyToGateways
so that this TextReplacement
will be
performed any time the FlowControl.Account account with the name "User" executes a function against the emulator.
This new TextReplacement
will be used when we execute a transaction using the contract we just deployed.
Transactions
First we'll write the transaction we want to execute.
_10string transaction = @"_10 import HelloWorld from %USERADDRESS% _10 transaction {_10 prepare(acct: AuthAccount) {_10 log(""Transaction Test"")_10 HelloWorld.hello(data:""Test Event"")_10 }_10 }";
Based on the TextReplacement
we created earlier, %USERADDRESS%
will be replaced with the Flow address
of the user account we created. This will then call the hello
function on the HelloWorld
contract
we deployed to the user account.
Next we follow a similar pattern to before:
_10Task<FlowTransactionResult> transactionTask = userAccount.SubmitAndWaitUntilSealed(transaction);_10yield return new WaitUntil(() => transactionTask.IsCompleted);_10_10if (transactionTask.Result.Error != null || !string.IsNullOrEmpty(transactionTask.Result.ErrorMessage))_10{_10 Debug.LogError($"Error executing transaction: {transactionTask.Result.Error?.Message??transactionTask.Result.ErrorMessage}");_10 yield break;_10}
Here, we're using the SubmitAndWaitUntilSealed
FlowControl function. This combines two SDK functions
together. It first submits the transaction to the network. Then it polls the network until the network
indicates that the transaction has been sealed and then returns the results.
Because this is combining two operations together, there are two potential failure points. The first
is a network error or syntax error that causes the submission to be rejected. This will be indicated
in the Result.Error
field. The second is something that goes wrong during the processing of the
transaction after submission was successful. This will be indicated in the Result.ErrorMessage field.
When using SubmitAndWaitUntilSealed or SubmitAndWaitUntilExecuted, you will want to check both of the
error fields to ensure it has completed successfully.
Finally, we check the events emitted by the transaction. Because submitting transactions returns before the transaction is actually processed, you can't return data directly from a transaction like you can with a script. Instead, you emit events that can be retrieved. We'll check the events of the completed transaction as follows:
Transaction Events
_10FlowEvent txEvent = transactionTask.Result.Events.Find(x => x.Type.Contains("TestEvent"));_10_10//Show that the transaction finished and display the value of the event that was emitted during execution._10//The Payload of the returned FlowEvent will be a CadenceComposite. We want the value associated with the_10//"field" field as a string_10Debug.Log($"Executed transaction. Event type: {txEvent.Type}. Event payload: {Convert.FromCadence<TestEvent>(txEvent.Payload).field}");
We end up a with a list of Events that were emitted by a transaction in the Result.Events
object. We
use LINQ to find the event we're interested in. It will contain "TestEvent" in it.
We need something to convert the Cadence TestEvent into, so we declared a C# class earlier:
_10public class TestEvent_10{_10 public String field;_10}
Then we have to get the payload from the event to display. We'll convert it into our TestEvent class and access the field
field.