[Sophia] Decoding return value on contract function


#1

I’d like some of the more experienced devs to take a look at this issue I have while decoding value returned by smart contract function.
Let’s say I have the following contracts:

Test.aes

contract TokenInterface =
  function balanceOf : (address) => (int)

contract Test =
  record state = {
    _token     : option(TokenInterface) }
  
  public stateful function init() : state = 
    { _token = None }

  public stateful function setToken(token: TokenInterface) = 
    put(state{ _token = Some(token) })

  public function getToken(): TokenInterface =
    switch(state._token)
      None => abort("Token not initialized!")
      Some(token) => token

… and Token.aes (conforms to TokenInterface)

contract Token =
    type state = ()

    public function balanceOf(wallet: address) : int =
        42

In my tests I deploy both contracts successfully, and after that I call setToken() function on Test contract where I pass the address of Token contract like this:

await deployedTestContract.call("setToken", {
	args: `("${deployedTokenContract.address}")`,
	options: {
		ttl: config.ttl
	}
})

I printed deployedTokenContract.address to standard output and it evaluates to correct address of deployed Token contract. As I’ve understood, I have to enclose that value in double-quotes and then provide it in form of a tuple when I’m calling function from forgAE test.

Then, I simply try to call getToken() function on Test contract, where I expect to get the same address from the step before (deployedTokenContract.address):

const fetchedToken = await deployedTestContract.call("getToken")
const decodedFetchedToken = await fetchedToken.decode("address")
console.log(decodedFetchedToken)

But the output is this:

{ type: 'word', value: 'ak_FXjQL6s' }

Now I think I have called setToken() in a correct way, because if I skip that step - getToken() call throws an exception (as expected because abort is called). Why am I getting this return value for my getToken() call?


#2

Thanks for the question, I’ve pinged @piwo to answer you


#3

Hello, this is a very good question and I am not perfectly sure what is going on here. I assume your deployedTokenContract.address is ak_...base58c... format, but contracts need addresses to be passed as address literal 0x...hex...

But I am unsure what ak_FXjQL6s is supposed to be.

Let me ask the core team and I’ll get back to you!


#4

Thanks for your patience, after I did try if for myself the issues were more clear.

  1. When you put args as ("${deployedTokenContract.address}") then you don’t need the "" around the address as it should be an address not string.
  2. When you put the args an address need to be encoded as such for the vm which you can do using const Crypto = require('@aeternity/aepp-sdk').Crypto; and Crypto.decodeBase58Check(YOUR_ADDRESS.split('_')[1]).toString('hex')
  3. When you await fetchedToken.decode("address") then the type to decode needs to be in parenthesis so .decode("(address)")
  4. Your function getToken doesnt return an address as you try to decode, but it returns a TokenInterface as described correctly in your contract. You can’t decode this using the api. But you could call the balanceOf function on it to see that it works as intended.

Stylewise functional languages often use snake_case instead of camelCase. Also your _token doesn’t need the underscore as it wouldn’t shadow a variablename that is used otherwise in the contract, also in functional languages token' would be the prefered notion for this.

I did adjust your contracts a little:
Token.aes

contract Token =
  type state = ()

  public function balance_of(wallet : address) : int =
    42

TokenTest.aes

contract TokenInterface =
  function balance_of : (address) => (int)

contract TokenTest =
  record state = { token : option(TokenInterface) }

  public stateful function init() : state =
    { token = None }

  public stateful function set_token(token: TokenInterface) =
    put(state{ token = Some(token) })

  public function get_token_balance(addr: address): int =
    switch(state.token)
      None => abort("Token not initialized!")
      Some(token) => token.balance_of(addr)

then I did (miss-)use the deploy.js from forgae to try it out using forgae deploy:

const Deployer = require('forgae').Deployer;
const Crypto = require('@aeternity/aepp-sdk').Crypto;

function decodeAddress(key) {
    const decoded58address = Crypto.decodeBase58Check(key.split('_')[1]).toString('hex');
    return `0x${decoded58address}`
}

const deploy = async (network, privateKey) => {
    let deployer = new Deployer(network, privateKey);

    let deployedToken = await deployer.deploy("./contracts/Token.aes");
    console.log("deployedToken", deployedToken);

    let deployedTest = await deployer.deploy("./contracts/TokenTest.aes");
    console.log("deployedTest", deployedTest);

    let callSetToken = await deployedTest.call("set_token", {args: `(${decodeAddress(deployedToken.address)})`});
    console.log("callSetToken", callSetToken);

    let callGetToken = await deployedTest.call("get_token_balance", {args: `(0x0)`}); // any address works as its not evaluated
    console.log("callGetToken", callGetToken);

    let callGetTokenDecode = await callGetToken.decode("(int)");
    console.log("callGetTokenDecode", callGetTokenDecode);
};

module.exports = {
    deploy
};

This does return 42 as expected.

Let me know if you have any more questions!


#5

Thank you very much for this question @filip , we’re going to have forgAE taking care of the cumbersome encoding/decoding for your really soon. Stay tuned.


#6

Thank you @piwo , things are much more clear for me now!


#7

Ok, so I tried this with your contract examples, both by (miss)using deploy.js and by doing everything from within tests. Indeed it works as expected in both cases, as you already mentioned.

Then I tried to apply this to my original contracts and something weird is happening. When I do everything from deploy.js I can query the token which was initialized by calling set_token(), as expected. But when I do the exact same thing in tests, operation fails while calling get_token_balance() function. Following error pops out:

Error: Invocation failed: _b3V0X29mX2dhc0caXYY=

I couldn’t dig much more of debug info other than this. I wasn’t sure why this was happening since everything works in deploy script, so I decided to check if contracts were deployed correctly in test environment. And they were, I checked transactions for both contracts, status was ok. Then I tried to call some test functions on both contracts, just to make sure that they were 100% correctly deployed.
I basically created this dummy function in both contracts:

  public function test() : int = 42

Calling this function failed on Token contract and succeeded on the other one. Operation fails (after hanging for some time on that step) with following error:

Error: Giving up after 10 blocks mined.

I’m not sure what’s going on here. Has anyone noticed any of these error messages so far?


#8

Hey, _b3V0X29mX2dhc0caXYY= stands for (base64-encoded) out_of_gas usually this doesn’t actually say you use to little gas, but actually some instructions do pass the compiler but references fail during execution.

Could you share your full example then I can check, otherwise I’d think you got some references wrong with the interface or types in the interface.


#9

As I was able from the code you provided me in private, the issue was that when deploying a contract using the js-sdk initState has to be passed instead of args.

As one of your contracts was not initialized you were not able to call its interface thus the _b3V0X29mX2dhc0caXYY= (out_of_gas). Also this did mean that you can’t call functions on it at all so the call to test() transaction was not accepted by miners.