Skip to main content

Calling contracts with the Unity SDK

Smart contracts are backend programs that run on blockchains. Smart contracts can do many tasks, but for gaming they have two main purposes:

  • They handle tokens, which are digital assets stored on the blockchain
  • They provide backend logic that users can trust because it cannot change

For more information about smart contracts on Tezos, see Smart contracts.

The Unity SDK can call any deployed Tezos or Etherlink contract just like any other Tezos or EVM client can.

  • To call a Tezos smart contract, the application must be connected to a Beacon or social wallet
  • To call an Etherlink smart contract, the application must be connected to a WalletConnect contract

Calling Tezos contracts

Smart contracts have one or more entrypoints, which are the different ways that it can be called, similar to a method or function in programming languages or an endpoint in an API. Therefore, to call a Tezos smart contract, you need:

  • Its address, which starts with KT1
  • The entrypoint to call
  • The parameter to pass to the entrypoint, which must be in the format that the entrypoint expects
  • An amount of tez tokens to send with the transaction, which can be zero or more

To call a contract, make sure that you are connected to a Beacon wallet. Then create an OperationRequest object with that information and pass it to the TezosAPI.RequestOperation() method. For example, this code calls a contract and passes the parameter 5 to its increment entrypoint. When the transaction completes successfully, it logs the hash of the transaction. You can use this hash to look up information about the transaction in a block explorer.

private async void Awake()
{
await TezosAPI.WaitUntilSDKInitialized();

_connectButton.onClick.AddListener(OnConnectClicked);
_disconnectButton.onClick.AddListener(OnDisconnectClicked);
_requestOperationButton.onClick.AddListener(OnRequestOperationClicked);

TezosAPI.OperationResulted += OperationResulted;
}

private async void OnRequestOperationClicked()
{
// Verify that the app is connected to an EVM wallet via WalletConnect
WalletProviderData walletProviderData = TezosAPI.GetWalletConnectionData();
if (walletProviderData.WalletType != WalletType.BEACON && !TezosAPI.IsSocialLoggedIn()) {
Debug.LogError("Connect to a Beacon or social wallet first.");
return;
}

try
{
var request = new OperationRequest
{
// Contract to call
Destination = "KT1R2LTg3mQoLvHtUjo2xSi7RMBUJ1sJkDiD",
// Entrypoint to call
EntryPoint = "increment",
// Parameter to pass, as a Michelson expression
Arg = new MichelineInt(5).ToJson(),
// Amount of tez to send with the transaction
Amount = "0",
};
var response = await TezosAPI.RequestOperation(request);
Debug.Log("Transaction hash: " + response.TransactionHash);
}
catch (Exception e) when (e is WalletOperationRejected or SocialOperationFailed)
{
Debug.LogError($"Operation failed: {e.Message}");
}
catch (Exception e)
{
Debug.LogError($"Unexpected error during operation: {e.Message}");
}
}

private void OperationResulted(OperationResponse operationResponse)
{
Debug.Log("Transaction hash: " + operationResponse.TransactionHash);
}

Encoding parameters

Tezos entrypoint parameters must be in Micheline JSON format, which is the format that the Michelson language uses for values. You can use the Netezos SDK to format Micheline parameters or construct them as JSON strings.

Encoding parameters with the Netezos Micheline SDK

Micheline primitives include:

  • Integers, as in new MichelineInt(1)
  • Strings, as in new MichelineString("Hello")
  • Bytes, as in new MichelineBytes(bytes")

As described in Complex data types, Micheline values are organized as a series of nested pairs in tree and comb formats. For example, if an entrypoint accepts an integer, a string, and a series of bytes as a nested pair, you can format the parameter like this:

string myStringToBytes = "Hello!";
var bytes = new byte[myStringToBytes.Length];

for (var i = 0; i < myStringToBytes.Length; i++)
{
bytes[i] = (byte)myStringToBytes[i];
}

var parameter = new MichelinePrim
{
Prim = PrimType.Pair,
Args = new List<IMicheline>
{
new MichelineInt(1),
new MichelineString("Hello"),
new MichelineBytes(bytes)
}
}.ToJson();

var request = new OperationRequest
{
Destination = "KT1PB9rp17qfL6RQR9ZUsKMm3NvbSoTopnwY",
EntryPoint = "intStringBytes",
Arg = parameter,
Amount = "0",
};
var response = await TezosAPI.RequestOperation(request);

Encoding parameters as JSON strings

Because the Arg field of the OperationRequest object accepts a JSON string, you can also use a raw Micheline-formatted JSON string. For example, the MichelinePrim object in the previous example looks like this as a string:

{
"prim": "Pair",
"args": [
{
"int": "1"
},
{
"string": "Hello"
},
{
"bytes": "48656c6c6f21"
}
]
}

Therefore, you can create a string literal with this JSON, escaping characters as necessary, and use it in the OperationRequest object, as in this example:

var jsonString = "{\"prim\":\"Pair\",\"args\":[{\"int\":\"1\"},{\"string\":\"Hello\"},{\"bytes\":\"48656c6c6f21\"}]}";

var request = new OperationRequest
{
Destination = "KT1PB9rp17qfL6RQR9ZUsKMm3NvbSoTopnwY",
EntryPoint = "intStringBytes",
Arg = jsonString,
Amount = "0",
};

Block explorers can help you format parameters. For example, assume an entrypoint that accepts a parameter that consists of a string followed by any number of pairs of an integer and a string. If you fill in values for this parameter on the Interact tab of Better Call Dev and click Execute > Raw JSON, it shows this Micheline value in JSON format:

{
"prim": "Pair",
"args": [
{
"string": "My string"
},
[
{
"prim": "Pair",
"args": [
{
"int": "5"
},
{
"string": "String one"
}
]
},
{
"prim": "Pair",
"args": [
{
"int": "9"
},
{
"string": "String two"
}
]
},
{
"prim": "Pair",
"args": [
{
"int": "12"
},
{
"string": "String three"
}
]
}
]
]
}

You can convert this JSON to a string and use it in the parameter instead of constructing the JSON with Netezos objects.

Calling Tezos views

To call a view, pass the address of the contract, the name of the view, and the Michelson-encoded parameter to the TezosAPI.ReadView() method. You must set the return type on the TezosAPI.ReadView() method, as in this example for a view that returns a string:

var result = await TezosAPI.ReadView<string>("KT1K46vZTMEe8bnacFvFQfgHtNDKniEauRMJ", "simple", "\"String value\"");
Debug.Log("View response: " + result);

If the return type is more complicated than a single primitive, you must create a type to represent the return type. For example, the FA2 contract KT1HP6uMwf829cDgwynZJ4rDvjLCZmfYjja1 has a view named get_balance_of that returns information about token owners. Block explorers such as tzkt.io show the parameter and return types for this view in JSON and Michelson format:

Parameter and return types for the view

The equivalent C# types look like these examples:

private class ParameterType
{
public string owner;
public int token_id;
}

private class ResponseType
{
public Request request { get; set; }
public string balance { get; set; }
}

public class Request
{
public string owner { get; set; }
public string token_id { get; set; }
}

This example shows how to use these types to call the view and receive the response:

var parameter = new List<ParameterType>
{
new()
{
owner = "tz1QCVQinE8iVj1H2fckqx6oiM85CNJSK9Sx",
token_id = 0
},
new()
{
owner = "tz1hQKqRPHmxET8du3fNACGyCG8kZRsXm2zD",
token_id = 0
}

};

var json = await TezosAPI.ReadView<List<ResponseType>>(
"KT1HP6uMwf829cDgwynZJ4rDvjLCZmfYjja1", "get_balance_of", parameter
);

foreach (var item in json)
{
Debug.Log($"The account {item.request.owner} has {item.balance} tokens of type {item.request.token_id}");
}

Like Tezos contracts, Etherlink smart contracts have functions that clients can call. To call an Etherlink smart contract, you need:

  • Its address
  • The entrypoint to call
  • The contract's application binary interface (ABI), which is a description of the contract's interface; you can get the ABI from the tool that deployed the contract or by compiling the source code of the contract in a tool such as the Remix IDE
  • The parameter to pass to the entrypoint
  • An amount of XTZ to send with the transaction, which can be zero or more

To call a contract, make sure that you are connected to a WalletConnect wallet. Then, create an OperationRequest object with the necessary information and pass it to the TezosAPI.RequestOperation() method. For example, this code calls a contract and passes the parameter 123 to its set entrypoint. When the transaction completes successfully, it logs the hash of the transaction. You can use this hash to look up information about the transaction in the Etherlink block explorer.

private async void Awake()
{
await TezosAPI.WaitUntilSDKInitialized();

_connectButton.onClick.AddListener(OnConnectClicked);
_disconnectButton.onClick.AddListener(OnDisconnectClicked);
_requestOperationButton.onClick.AddListener(OnRequestOperationClicked);

TezosAPI.OperationResulted += OperationResulted;
}

private async void OnRequestOperationClicked()
{
// Verify that the app is connected to an EVM wallet via WalletConnect
WalletProviderData walletProviderData = TezosAPI.GetWalletConnectionData();
if (walletProviderData.WalletType != WalletType.WALLETCONNECT) {
Debug.LogError("Connect to a WalletConnect wallet first.");
return;
}

try
{
var request = new OperationRequest
{
// Contract to call
Destination = "0xfac1791E9db153ef693c68d142Cf11135b8270B9",
// Entrypoint to call
EntryPoint = "set",
// ABI of contract
ContractABI = "[ { \"inputs\": [], \"name\": \"get\", \"outputs\": [ { \"internalType\": \"uint256\", \"name\": \"\", \"type\": \"uint256\" } ], \"stateMutability\": \"view\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"uint256\", \"name\": \"x\", \"type\": \"uint256\" } ], \"name\": \"set\", \"outputs\": [], \"stateMutability\": \"nonpayable\", \"type\": \"function\" } ]",
// Parameter to pass
Arg = "129",
// Amount of XTZ to send with the transaction
Amount = "0",
};
var response = await TezosAPI.RequestOperation(request);
Debug.Log("Transaction hash: " + response.TransactionHash);
}
catch (Exception e) when (e is WalletOperationRejected)
{
Debug.LogError($"Operation failed: {e.Message}");
}
catch (Exception e)
{
Debug.LogError($"Unexpected error during operation: {e.Message}");
}
}

private void OperationResulted(OperationResponse operationResponse)
{
Debug.Log("Transaction hash: " + operationResponse.TransactionHash);
}