Ethereum Unity3D Boilerplate Questions

thanks for the clarification !
What if we build a unity webgl dapp set to run in background ?

Hello again. Thanks for all your help on this. We’re still struggling to figure out what we might be doing wrong. The code works perfectly in the Unity Editor with the build target set to PC / Mac / Linux stand-alone, but not with WebGL as the build target (both as an actual build and in the editor).

What we see when we run with WebGL as the build target is the following:

  1. Two new records are created in the DB but are not updated or deleted.
  2. There is a NullReferenceException thrown on line 108 of UniversalWebClient.cs
  3. None of the Debug.Log($"***** …") statements appear in the console (although we do see several WebRequest entries in the log).

Here’s the final test code based on what you posted with just a few minor adjustments to the WebGL portions. (Also, to make the compiler happy, we had to put the MoralisLiveQueryController.cs and MoralisSubscriptionQuery.cs in the WebGL namespace):

Thank you!

using Assets.Scripts;
using MoralisWeb3ApiSdk;
using System;
using UnityEngine;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
#if UNITY_WEBGL
using Cysharp.Threading.Tasks;
using Moralis.WebGL;
using Moralis.WebGL.Platform.Objects;
using Moralis.WebGL.Platform.Queries;
using Moralis.WebGL.Platform.Queries.Live;
#else
using Moralis;
using Moralis.Platform.Objects;
using Moralis.Platform.Queries;
using Moralis.Platform.Queries.Live;
#endif

public class LiveQueryTest
{
#if UNITY_WEBGL
    public async void TestLiveQuery()
    {
        var moralisQueryPlayerData = await MoralisInterface.GetClient().Query<PlayerData>();

        // Setup subscription
        setupLiveQuerySubscription(moralisQueryPlayerData);

        Thread.Sleep(2000);
#else
    public async void TestLiveQuery()
    {
        var moralisQueryPlayerData = MoralisInterface.GetClient().Query<PlayerData>();

        // Setup subscription
        setupLiveQuerySubscription(moralisQueryPlayerData);

        Thread.Sleep(2000);
#endif
        System.Random rand = new System.Random((int)DateTime.Now.Ticks);

        int x = rand.Next(25) + 3;

        PlayerData p1 = MoralisInterface.GetClient().Create<PlayerData>();
        p1.Name = GetTestName();
        p1.TokenCount = x;
        await p1.SaveAsync();

        x = rand.Next(25) + 3;
        
        PlayerData p2 = MoralisInterface.GetClient().Create<PlayerData>();
        p2.Name = GetTestName();
        p2.TokenCount = x;
        await p2.SaveAsync();

        // Get the records created
        IEnumerable<PlayerData> recs = await moralisQueryPlayerData.FindAsync();

        // Update data
        foreach (PlayerData pd in recs)
        {
            x = rand.Next(25) + 3;
            pd.TokenCount = x;
            await pd.SaveAsync();
        }

        // Delete data
        foreach (PlayerData pd in recs)
        {
            await pd.DeleteAsync();
        }
    }

    private void setupLiveQuerySubscription(MoralisQuery<PlayerData> playerData)
    {
        MoralisLiveQueryCallbacks<PlayerData> moralisLiveQueryCallbacks = new MoralisLiveQueryCallbacks<PlayerData>();

        moralisLiveQueryCallbacks.OnConnectedEvent += (() => { Debug.Log("Connection Established."); });
        moralisLiveQueryCallbacks.OnSubscribedEvent += ((requestId) => { Debug.Log($"Subscription {requestId} created."); });
        moralisLiveQueryCallbacks.OnUnsubscribedEvent += ((requestId) => { Debug.Log($"Unsubscribed from {requestId}."); });
        moralisLiveQueryCallbacks.OnErrorEvent += ((ErrorMessage em) =>
        {
            Debug.Log($"***** ERROR: code: {em.code}, msg: {em.error}, requestId: {em.requestId}");
        });
        moralisLiveQueryCallbacks.OnCreateEvent += ((item, requestId) =>
        {
            Debug.Log($"***** Created ");
        });
        moralisLiveQueryCallbacks.OnUpdateEvent += ((item, requestId) =>
        {
            Debug.Log($"***** Updated ");
        });
        moralisLiveQueryCallbacks.OnDeleteEvent += ((item, requestId) =>
        {
            Debug.Log($"***** Deleted");
        });
        moralisLiveQueryCallbacks.OnGeneralMessageEvent += ((text) =>
        {
            Debug.Log($"***** Websocket message: {text}");
        });

        MoralisLiveQueryController.AddSubscription<PlayerData>("PlayerData", playerData, moralisLiveQueryCallbacks);
    }

    private static string GetTestName()
    {
        string[] names = { "Clem the Great", "Sion the Bold", "Bob", "D@ve", "Oogmar the Deft", "Alesdair the Blessed", "Seviel the Mighty", "Master Adept Xactant", "Semaphore the Beautiful", "Gamemaster Nexnang" };

        System.Random rand = new System.Random((int)DateTime.Now.Ticks);

        int x = rand.Next(names.Length);

        x = rand.Next(names.Length);

        return names[x];
    }
}

** Software Versions Used
Unity 2020.3.24f1
Moralis Ethereum Unity Boilerplate v1.0.8

1 Like

Hi there,
took this from the git :

// No parameters
object[] pars = new object[0];
string jsonResult = await f.CallAsync(pars);

show an error :

Argument 1: cannot convert from ‘object[]’ to ‘Nethereum.RPC.Eth.DTOs.CallInput’

what am I missing ? :confused:

1 Like

Which Web3API operation are you trying to call?

I will check this out …

Based on this statement you included, while I am setting this up, please check the logs on your server to see if there were any errors (esp. table permissions for update / delete)

  1. So it looks like create and update were working fine as was the delete. However it looks like for the Delete function the response is null with a status of 200 but the WebGL UniversalWebClient did not handle that properly.

I need to create an update for this. To get you by for now, in the WebGL UniversalWebClient make these two changes:
A) Change line 94 to

string responseText = "{}";

B) Change line 108 to:

responseText = webRequest.downloadHandler == null ? responseText : webRequest.downloadHandler.text;
  1. The Callbacks are not firing for WebGL, digging into that next.

Hey, some of the readme are out dated
what are you trying to do ?

1 Like

@jcm

Opened issue #81 for the callbacks not working for WebGL.

1 Like

OK - just checked the logs & they look good. Nothing in there from yesterday or today, just a few unrelated errors that were logged on the 6th. Thanks!

1 Like

Thank you very much!

1 Like

Arf Ok !

I am struggling hard to communicate with my marketplace smart contract.
It works well on remix, everything seems ready. But I can’t get it to work.

I made a listing of NFTs (with token price and IDs) and trying to fetch infos.
Capture d’écran 2022-03-09 à 01.34.18
This “offers” function takes in one input (the listing ID) and returns the token price that should be displayed within unity.

More generally I need to understand how to use the RunContractFunction. And the readme is very unclear to me.

Thanks for your support.

1 Like

Is it on a test network?

If so, please provide the chain, the function ABI, the contract address and a listing ID for which you expect to receive data?

Thank you,

David

it’s on mumbai test net.

The abi is this:

[
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "_nftCollection",
				"type": "address"
			},
			{
				"internalType": "address",
				"name": "_tokenCollection",
				"type": "address"
			}
		],
		"stateMutability": "nonpayable",
		"type": "constructor"
	},
	{
		"anonymous": false,
		"inputs": [
			{
				"indexed": false,
				"internalType": "address",
				"name": "user",
				"type": "address"
			},
			{
				"indexed": false,
				"internalType": "uint256",
				"name": "amount",
				"type": "uint256"
			}
		],
		"name": "ClaimFunds",
		"type": "event"
	},
	{
		"anonymous": false,
		"inputs": [
			{
				"indexed": false,
				"internalType": "uint256",
				"name": "offerId",
				"type": "uint256"
			},
			{
				"indexed": false,
				"internalType": "uint256",
				"name": "token_id",
				"type": "uint256"
			},
			{
				"indexed": false,
				"internalType": "address",
				"name": "user",
				"type": "address"
			},
			{
				"indexed": false,
				"internalType": "uint256",
				"name": "price",
				"type": "uint256"
			},
			{
				"indexed": false,
				"internalType": "bool",
				"name": "fulfilled",
				"type": "bool"
			}
		],
		"name": "Offer",
		"type": "event"
	},
	{
		"anonymous": false,
		"inputs": [
			{
				"indexed": false,
				"internalType": "uint256",
				"name": "offerId",
				"type": "uint256"
			},
			{
				"indexed": false,
				"internalType": "uint256",
				"name": "id",
				"type": "uint256"
			},
			{
				"indexed": false,
				"internalType": "address",
				"name": "owner",
				"type": "address"
			}
		],
		"name": "OfferCancelled",
		"type": "event"
	},
	{
		"anonymous": false,
		"inputs": [
			{
				"indexed": false,
				"internalType": "uint256",
				"name": "offerId",
				"type": "uint256"
			},
			{
				"indexed": false,
				"internalType": "uint256",
				"name": "id",
				"type": "uint256"
			},
			{
				"indexed": false,
				"internalType": "address",
				"name": "newOwner",
				"type": "address"
			}
		],
		"name": "OfferFilled",
		"type": "event"
	},
	{
		"anonymous": false,
		"inputs": [
			{
				"indexed": true,
				"internalType": "address",
				"name": "previousOwner",
				"type": "address"
			},
			{
				"indexed": true,
				"internalType": "address",
				"name": "newOwner",
				"type": "address"
			}
		],
		"name": "OwnershipTransferred",
		"type": "event"
	},
	{
		"stateMutability": "nonpayable",
		"type": "fallback"
	},
	{
		"inputs": [
			{
				"internalType": "uint256",
				"name": "_offerId",
				"type": "uint256"
			},
			{
				"internalType": "uint256",
				"name": "price",
				"type": "uint256"
			}
		],
		"name": "BuyNFT",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "string",
				"name": "_tokenURI",
				"type": "string"
			},
			{
				"internalType": "uint256",
				"name": "amount",
				"type": "uint256"
			},
			{
				"internalType": "uint256",
				"name": "price",
				"type": "uint256"
			}
		],
		"name": "SetPrice",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "to",
				"type": "address"
			},
			{
				"internalType": "uint256",
				"name": "amount",
				"type": "uint256"
			}
		],
		"name": "Withdraw",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "offerCount",
		"outputs": [
			{
				"internalType": "uint256",
				"name": "",
				"type": "uint256"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "uint256",
				"name": "",
				"type": "uint256"
			}
		],
		"name": "offers",
		"outputs": [
			{
				"internalType": "uint256",
				"name": "offerId",
				"type": "uint256"
			},
			{
				"internalType": "uint256",
				"name": "id",
				"type": "uint256"
			},
			{
				"internalType": "address",
				"name": "user",
				"type": "address"
			},
			{
				"internalType": "uint256",
				"name": "price",
				"type": "uint256"
			},
			{
				"internalType": "bool",
				"name": "fulfilled",
				"type": "bool"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "",
				"type": "address"
			},
			{
				"internalType": "address",
				"name": "",
				"type": "address"
			},
			{
				"internalType": "uint256[]",
				"name": "",
				"type": "uint256[]"
			},
			{
				"internalType": "uint256[]",
				"name": "",
				"type": "uint256[]"
			},
			{
				"internalType": "bytes",
				"name": "",
				"type": "bytes"
			}
		],
		"name": "onERC1155BatchReceived",
		"outputs": [
			{
				"internalType": "bytes4",
				"name": "",
				"type": "bytes4"
			}
		],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "",
				"type": "address"
			},
			{
				"internalType": "address",
				"name": "",
				"type": "address"
			},
			{
				"internalType": "uint256",
				"name": "",
				"type": "uint256"
			},
			{
				"internalType": "uint256",
				"name": "",
				"type": "uint256"
			},
			{
				"internalType": "bytes",
				"name": "",
				"type": "bytes"
			}
		],
		"name": "onERC1155Received",
		"outputs": [
			{
				"internalType": "bytes4",
				"name": "",
				"type": "bytes4"
			}
		],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "owner",
		"outputs": [
			{
				"internalType": "address",
				"name": "",
				"type": "address"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "renounceOwnership",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "bytes4",
				"name": "interfaceId",
				"type": "bytes4"
			}
		],
		"name": "supportsInterface",
		"outputs": [
			{
				"internalType": "bool",
				"name": "",
				"type": "bool"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "tokenCount",
		"outputs": [
			{
				"internalType": "uint256",
				"name": "",
				"type": "uint256"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "newOwner",
				"type": "address"
			}
		],
		"name": "transferOwnership",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	}
]

The contract address is : 0x383cAe6B39ad82305242EFcfDa6EC5B2a52B4620

and listing function is called “Offers” with ID = 1.

Thank you so very much!

1 Like

First - an issue I see is that the single input for the offers function does not have a name.

I used the contract address you sent on etherscan to find the contract and decompile it. This function decompiles to

def offers(uint256 _param1) payable: 
  require calldata.size - 4 >=′ 32
  require _param1 == _param1
  return offers[_param1].field_0, 
         offers[_param1].field_256,
         offers[_param1].field_512,
         offers[_param1].field_768,
         bool(offers[_param1].field_1024)

_param1 is not the actual name. Guessing based on this statement:

That _param1 is “id”. Is this correct?

Thanks a lot for the clarification:
That _param1 is indeed the “offerId”.

1 Like

Here is how you will call:

            // Function ABI input parameters
            object[] inputParams = new object[1];
            inputParams[0] = new { internalType = "uint256", name = "id", type = "uint256" };

            // Function ABI Output parameters
            object[] outputParams = new object[5];
            outputParams[0] = new { internalType = "uint256", name = "offerId", type = "uint256" };
            outputParams[1] = new { internalType = "uint256", name = "id", type = "uint256" };
            outputParams[2] = new { internalType = "address", name = "user", type = "address" };
            outputParams[3] = new { internalType = "uint256", name = "price", type = "uint256" };
            outputParams[4] = new { internalType = "bool", name = "fulfilled", type = "bool" };

            // Function ABI
            object[] abi = new object[1];
            abi[0] = new { inputs = inputParams, name = "offers", outputs = outputParams, stateMutability = "view", type = "function" };


            // Define request object
            RunContractDto rcd = new RunContractDto()
            {
                Abi = abi,
                Params = new { id = "1" }
            };

            object resp = await MoralisInterface.GetClient().Web3Api.Native.RunContractFunction("0x383cAe6B39ad82305242EFcfDa6EC5B2a52B4620", "offers", rcd, ChainList.mumbai);

However I found an issue with the call for functions that return multiple items. Created an update. About to push v1.0.9 that contains this fix and a few other outstanding defects and feature requests. Check the github repo in about 30 minutes (currently 17:34 UTC)

EDITED: Release 1.0.9 has been published.

1 Like

Woooow it really works !!! :))))) So powerful Thank you so very much !!

Question1: So If I want to fetch only one output parameter from those 5 (lets say “price”), I would have to read through the string output ?

Question2: When I try to use a custom write function like “IncreaseAllowance” this error shows :

ApiException: Error calling RunContractFunction: {“code”:141,“error”:“Only read functions are allowed”}

The readme says that it “Runs a given function of a contract abi and returns readonly data”, is there a way to change that ? I see in the git that there is a GetTokenAllowance function, but is there a way to Set the token allowance ?

Most importantly, I have a custom BuyNFT function in my smart contract that returns the same error (I want users to buy NFTs with my custom ERC20).

Thanks again for the amazing work and support!

1 Like

Since the result is json you can either use a JsonReader to extract values or (what I prefer) create a response object and deserialize the json to that object. Infact this may be a good future feature, a generic version that allows you to call this as

RunContractFunction<MyResultObject>( ... 

Also I think another good feature would be to allow the consumer to pass in the ABI as JSON (since that is how it is usually provided) instead of going through the process of defining the inputs, outputs, etc.

That is correct for the Web3API - only read methods can be called this way. Write functions need to go through Wallet Connect or Web3. The cade in the demo (AwaradableController.cs) provides a good example of how to do this. Here is the relevant code but see the class for the inputs, etc.:

#if UNITY_WEBGL

            // Convert token id to hex as this is what the contract call expects
            object[] pars = new object[] { bi.ToString() };

            // Set gas estimate
            HexBigInteger gas = new HexBigInteger(0);
            string resp = await MoralisInterface.ExecuteFunction(Constants.MUG_CONTRACT_ADDRESS, Constants.MUG_ABI, Constants.MUG_CLAIM_FUNCTION, pars, new HexBigInteger("0x0"), gas, gas);
#else

            // Convert token id to hex as this is what the contract call expects
            object[] pars = new object[] { bi.ToString("x") };

            // Set gas estimate
            HexBigInteger gas = new HexBigInteger(0);
            // Call the contract to claim the NFT reward.
            string resp = await MoralisInterface.SendEvmTransactionAsync("Rewards", "mumbai", "claimReward", addr, gas, new HexBigInteger("0x0"), pars);
#endif

FYI - The format of the non-WebGL code will be updated to match the format of the WebGL code in the near future.

1 Like

I know that we can call a contract function using this method

       // Calling: function balanceOf(address account, uint256 id) external view returns (uint256);
       // Function ABI input parameters
        object[] inputParams = new object[1];
        inputParams[0] = new { internalType = "address", name = "account", type = "address" };

        // Function ABI Output parameters
        object[] outputParams = new object[1];
        outputParams[0] = new { internalType = "uint256", name = "", type = "uint256" };

        // Function ABI
        object[] abi = new object[1];
        abi[0] = new { inputs = inputParams, name = "balanceOf", outputs = outputParams, stateMutability = "view", type = "function" };


        // Define request object
        string accountAddress , ERC20ContractAddress;
        RunContractDto rcd = new RunContractDto()
        {
            Abi = abi,
            Params = new { account = accountAddress }
        };

        string resp = await MoralisInterface.GetClient().Web3Api.Native.RunContractFunction(ERC20ContractAddress, "balanceOf", rcd, ChainList.mumbai);

But how can I use array[] as input and get array[] as output for a read-only function? Like this;
function isWhiteListed(address[] calldata addr) external view returns (uint256[] memory result);

1 Like