4

I added comments to detail the question in hand and my attempts to solve it.

pragma solidity ^0.4.19;

contract Users {
    // a user with some metadata
    struct User {
        bytes32 name;
        bytes32 email;
    }

    // users are aggregated (notice the array) by a unique key  
    mapping(uint => User[]) internal users;

    // and of course we keep track of these unique keys
    // because we can't loop through a mapping
    uint[] internal keys;

    function signUp(bytes32 name, bytes32 email) public {
        // our unique key, users are aggregated by day
        uint key = now / 1 days;

        // if this is the first user within a key, we keep track of that key
        if (users[key].length == 0) keys.push(key);

        // we push the new user to the key's array of users
        users[key].push(User(name, email));
    }

    // so far so good, now we move on to retrieving the data

    // this answers the question of: in what keys there are users?
    // one call, we get all the keys, efficient
    function getKeys() public view returns (uint[]) {
        return keys;
    }

    // now we need to answer the question of: who are the users of the key x?

    // throws, Error: Unsupported or invalid type: tuple
    function getUsers(uint key) public view returns(User[]) {
        return users[key];
    }

    // the following approach works
    // but requires users[key].length calls to the network, not efficient

    // first, we need the number of indices within a key
    function getUsersCount(uint key) public view returns (uint) {
        return users[key].length;
    }

    // then, we need to loop getUsersCount(key) times
    // to retrieve the metadata for each user

    // throws, Error: Unsupported or invalid type: tuple
    function getUser(uint key, uint index) public view returns(User) {
        return users[key][index];
    }

    // same error from before, let's try this
    function getUserModified(uint key, uint index) public view returns(bytes32, bytes32) {
        User memory user = users[key][index];
        return (user.name, user.email);
    }

    // works ..
    // aha! so returns(User) is not supported, we have to destruct
    // or is this limitation of Remix only?

    // the question now is ..
    // how can we return an array of (user.name, user.email)?
    // that is, modifying faulty getUsers(), to avoid multiple getUserModified() calls
}
user5470921
  • 145
  • 4

1 Answers1

1

Currently, structs cannot be returned in public/external contract methods.. only internally. As you've noted in your example above (getUserModified), you can return multiple variables, but not particularly helpful for handling a list. That leaves you with returning a list of modified user keys, and then iterating through the list and pulling the name/email associated with each key.

Update: See comment below by user5470921 .. it appears that returning an array of structs is possible when using the ethers lib (rather than web3.js) with solidity ^0.4.19. Just be aware that any web clients running web3.js will have issues.

Howard
  • 421
  • 2
  • 6
  • How would you recommend architecting the contract to achieve the desired result? I can see how we can do bytes32[] names; bytes32[] emails[]; and not use structs at all, but that doesn't look good to me, is this how all apps do it nowadays? what if we have 10k users, that network call would be very costly with this new design – user5470921 Feb 13 '18 at 20:36
  • I would probably keep the user struct, and add a separate array that holds Users. I would have your key to User[] mapping be a key to uint[] mapping.. ie key to an array of user ids. – Howard Feb 13 '18 at 21:22
  • The getUsers method would return the array of user ids, but yes, getting data for every user would be a lot of network calls. Also.. are you sure that bytes32 will be sufficient to store your users email addresses? A possibility here could be to storing the user data off-chain. Create a hash of the user record and store the bytes32 hash in your contract. Use that to pull full user data from elsewhere. – Howard Feb 13 '18 at 21:28
  • So basically we run into the same issue, too many network calls. Is this a known issue or is the support not there yet and it will be introduced later on (calling nested goodies externally)? And yes (about your email remark) I wrote this example just to explain what I am trying to achieve it's not the actual code. The off chain idea is a good one, but in this case we actually would like the data itself on the blockchain. – user5470921 Feb 13 '18 at 23:40
  • It's strange because the support for returning a single typical array uint[] foo; is already there, but returning an array within a struct or an array within a mapping (I am guessing also a nested array within an array) seems to be cumbersome. – user5470921 Feb 13 '18 at 23:42
  • Supposedly its coming.. https://github.com/ethereum/solidity/issues/2948 – Howard Feb 14 '18 at 01:38
  • Yep been trying to get it to work with ethers for the past hour as it has full support for it already, wonderful library I must say, I already like it better than web3. So it's not a solidity thing after all, no luck so far I'll update here if I end up with a positive outcome. – user5470921 Feb 14 '18 at 02:17
  • I can confirm that returning an array of structs is fully supported with ethers and solidity ^0.4.19. I will accept this as an answer, but could you possibly add an update indicating that it is now a possibility? for anyone coming up from a search, could save them some time. – user5470921 Feb 15 '18 at 23:28
  • Note: pragma experimental ABIEncoderV2; would still be needed for now. – user5470921 Feb 16 '18 at 03:15