Web3 Wallets in Unity

How to provide users with their web3 identity in a Unity game

This tutorial will show how to create a Web3 wallet embedded within a Unity application, with just a few short lines of code.

If you are already familiar with the concept of a wallet in the Web3 world, you can skip the Introduction section and go straight ahead to the code.

Introduction

One of the main requirements for adding a Web3 element to any game is user identification. While the traditional 'Web2' methods of user account identification (usernames, emails, passwords etc) are all still valid, where true digital ownership is concerned, these data are not sufficient. Identification of users in a Web3 environment requires each user to have one or more Web3 accounts - popularly referred to as ‘wallets’. While the specifics of the account differ between different blockchains, all follow the basic schema below:

Every gamer who wishes to trade in-game assets tokenized on the blockchain must have an account that follows this structure - if not, then ownership of assets cannot be demonstrated, and this risks the game (or game developer) having to store the ownership on behalf of the owner (and, essentially, becoming a bank).

Private keys should be kept as secure as possible. If a private key is lost, there is no way for any individual to demonstrate ownership of assets associated with that account; essentially, the assets are lost forever. In the same way, if a private key is stolen, another user can stole all the assets stored in the wallet.

Custodial vs Non-Custodial wallets

Web3 wallets can be divided into two conceptual categories:

  1. Non-custodial wallets: where the private key is stored locally (on the user's end device). If the private key is lost or compromised, it is the user's responsibility. Non-custodial wallets are easier for most games to implement, as they require no less legal compliance. However, they provide a UX challenge to appeal to Web2-savvy users, who are used to being able to reset access passwords for most services with a single click.

  2. Custodial wallets: where the responsibility for the private key is taken by a 3rd party. The great advantage of this is that if the user loses or forgets a password, steps can be taken to confirm their identity and restore access. However, the legal situation is more complicated, as the custodial wallet issuer is assuming legal responsibility for holding the assets of the user.

Wallet choice

There are several ways to implement or integrate a wallet into a game. In this tutorial, we will show how easy it is to build a lightweight, non-custodial 'onboarding' wallet into any Unity application, with just a few lines of code. This option is great because it allows you to create a wallet on behalf of the user without them having to do anything - no clicks, no installing, no logging in; nothing.

The disadvantage is that the private key is stored alongside with the game binary, and if the user deletes the game or tampers with configuration files, then the private key could be easily lost. So we must take steps to protect against this.

So let's get started!

Requirements

  • Basic knowledge of development in Unity and C#.

  • The latest version of Unity is installed on your development machine.

  • The latest 'common' dll's for Nethereum (this tutorial was made with version 4.14.0, specifically with the net472UnityCommonAOT dlls - direct link). Nethereum is an open-source .NET integration for Ethereum.

Setup

Start by creating a new Unity project, naming it whatever you want, and leaving all settings as default. (Or alternatively, use an existing project - no changes will affect the rest of the application directly).

Unzip the downloaded Nethereum archive, and drag and drop the entire folder into the Assets folder within the Unity application. Unity will now compile all the new files, and leave the libraries ready to use.

Finally, install the Newtonsoft JSON Serialization library into the Unity project. In Unity, go to Window->Package Manager, once the Package Manager window opens, go to Add package from git URL, type com.unity.nuget.newtonsoft-json press Add and done.

Building an Onboarding Wallet into a Unity application

A Web3 wallet with a single line of code

Create a new Unity script called KeyStore and associate it with any GameObject within your scene. Open the code file and update the template code to the following:

// ensure all the correct dependencies are present
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Nethereum.Signer;
using Nethereum.Hex.HexConvertors.Extensions;
using System.IO;
using Newtonsoft.Json;

public class KeyStore : MonoBehaviour
{
    void Start()
    {
        // Create a new wallet and print the public address and private key
        EthECKey ecKey = EthECKey.GenerateKey();
        print(ecKey.GetPublicAddress());
        print(ecKey.GetPrivateKey());
    }
}

Now press play and watch the console.

Congratulations! A wallet for the player is created, with just a single line of code!

We have printed out two of the three elements of the wallet to the console. The public address (aka Web3 address or Ethereum address) is what the world sees. For example, it's what the Living Assets API uses to identify users, know which assets they own etc.

The private key is where the difficulties start: it must be stored safely in a way that only the user can access it. The problem at the moment is that the account is only stored in memory, when the application exits, both the public address and private key will be lost forever. Let's see how to mitigate this.

Safely storing the private key on the user's device

There are a couple of ways to store persistent data in Unity (PlayerPrefs and Application.persistantDataPath), but in their raw form, neither are ideal.

Both PlayerPrefs and persistantDataPath files are both inherently highly insecure. Data is stored in plain text, in an easy-to-find location on the local device, and can be read by anybody or, potentially, any application with access to the device. So, whichever way we decide to store the data, we must encrypt it first.

While we can use any encryption method we like, for this tutorial we will use an AES encryption method which is compatible across all Freeverse services. This will allow us, for example, to export the account for easy and quick trading in the Freeverse Customizable Marketplace at a later date.

Download Freeverse's AESEncryption C# class and drag into the Unity Assets folder.

When we encrypt the private key, we must naturally use some form of password:

  • The 'most secure' way is to ask the user for a password via the game UI. The disadvantage of this is that the whole point of this approach to creating a wallet is not to bother the user.

  • Another way is to use a unique device ID as the password. While this avoids bothering the user, it does mean that any application on the user's device, if it has access to the location of the saved json, could, in theory, extract the private key, as the device ID is unique to the device, not unique to our game.

  • An intermediary step is to combine another form of user identification (a username, an email address) combined the device ID. This means a malicious application would need to know this information, as well as knowing that the device ID is required. It would also need to know how both strings are combined - for example, the username could be interleaved character by character in the device ID, and understanding this method would require reverse engineering game binary.

This tutorial will just stay with a hard-coded password, on the understanding that in your game you will have to use a different approach.

Firstly, in order to serialize our data to JSON format to write to file, we need to create a utility class to serialize. We can add this in KeyStore.cs, outside of the KeyStore class:

// this class is used to store data for JSON serialization
public class KeyPair {
    public string EncryptedPvk;
    public string PublicKey;
    public string Address;

    public KeyPair(string encrypted, string publicKey, string address) {
        EncryptedPvk = encrypted;
        PublicKey = publicKey;
        Address = address;
    }
}

To store the created json, we can use Application.persistantDataPath. Let's take advantage of this to move our code from the Start() function into a new standalone function.

Within this function, once we create the new wallet, we will encrypt the private key with a test password, create a new object of our utility class to store that data, then serialize to JSON and write to a file.

// ...
    void CreateAndStoreKeys() 
    {
        // create new wallet, and encrypt the private key
        EthECKey ecKey = EthECKey.GenerateKey();
        string pvk = ecKey.GetPrivateKey();
        string encrypted = AESEncryption.AESEncrypt(pvk, "testPassword");

        // create object with the data we wish to store in JSON format
        KeyPair keyPair = new KeyPair(
            encrypted,
            ecKey.GetPubKey().ToHex(),
            ecKey.GetPublicAddress()
        );

        // serialize to JSON, and write to file
        var json = JsonConvert.SerializeObject(keyPair);        
        string jsonFile = Application.persistentDataPath + "/data.json";
        File.WriteAllText(jsonFile, json);

        print("Key created and stored");
    }
// ...

We can now create an equivalent function to read and decrypt the data:

// ...
    void LoadKeysFromStore()
    {
        //read stored file
        string jsonFile = Application.persistentDataPath + "/data.json";
        string json = File.ReadAllText(jsonFile);

        // deserialize JSON and decrypt to obtain private key
        KeyPair keyPair = JsonConvert.DeserializeObject<KeyPair>(json);
        string decrypted = AESEncryption.AESDecrypt(keyPair.EncryptedPvk, "testPassword");
        
        // print result to console
        print("Private Key " + decrypted);

        print("Key successfully read and decrypted");
    }
// ...

Finally, we can add a bit of logic to our Start() function to create new keys if no file is detected, and load the keys if the file does exist:

void Start()
{
    if (File.Exists(Application.persistentDataPath + "/data.json"))
    {
        LoadKeysFromStore();
    } 
    else 
    {
        CreateAndStoreKeys();
    }
}

Finishing up

We can now make our class a bit more useful by adding some getters and setters for the key data, so we can access it quickly from other areas of our code base in the future.

// ... add getters and setters at the start of the KeyStore class
public class KeyStore : MonoBehaviour
{
    public string PrivateKey { get; set; }
    public string PublicKey { get; set; }
    public string EncryptedID { get; set; }
    public string Address { get; set; }

Remember to set these variables in the LoadKeysFromStore() and CreateAndStoreKeys() functions!

And that's it! With just a few lines of code, we have the basic way of creating and storing Ethereum compatible keys within a Unity application.

Exporting the key for backup, and use with the Freeverse Customizable Marketplace

The same code that generated the JSON above can be used to export the key so that it can be backed-up, or so the user can access their wallet if they change device, or so the user can quickly do a one-click import to the Freeverse Customizable Marketplace to trade their Living Assets.

In this scenario, a user-specified password is essential.

Our suggestion for wallet export is to do so 'on-demand'. For example:

  • Create a wallet using this method the first time a new user opens the game. Do not ask them for a password, or ask them to export the wallet.

  • When the user acquires a Living Asset that may have potential value (either by buying it, or earning it within the game), use this moment to educate the gamer, and get them to set a password and export the wallet.

  • When the user begins acquiring items of very high value, use this moment to suggest exporting the wallet to a more secure 3rd party solution.

Using 3rd party wallet plugins

While the technique described above is great in that it hides the creation of the wallet from the user, it has some fairly serious security flaws - namely, the reliance on a single, probably short password, which can easily be lost. Furthermore, if stolen, it can't be invalidated. (For this reason, we encourage users who are beginning to trade with more valuable assets to pass from the Onboarding Wallet to more secure solutions).

There are several great options for 3rd party wallets - at Freeverse, we are agnostic as to the final solution that the game developer chooses, so long as the wallet is Ethereum compatible.

That said, our Customizable Marketplace has native support for both Metamask and Wallet Connect, both of which have Unity SDKs. See:

Conclusions

In this tutorial, we've seen how easy it is to create a lightweight non-custodial wallet in a Unity application. The great advantage of this approach is that it requires little or no interaction with the user. We've also discussed strategies to encourage the user to back up their keys and, once they begin to accrue real value, export the wallet to more secure solutions.

The full code for the tutorial can be found in our Examples repository. Thanks for following along!

Last updated

freeverse.io