Search code examples
ethereumsolidityweb3js

How to save and retrieve data on Ethereum blockchain with Solidity and Web.js


The below code only returns a receipt but I want it to return a tuple of data, like in the contract below. How do I get it to return the data? I can't find a good tutorial on how to save and retrieve data. I know this is an expensive use case, I'm just trying to do a basic proof of concept and learn at the same time.

I'm using [email protected]

export class AppComponent {
  title = 'app';
  dappUrl: string = 'http://myapp.com';
  web3: any;
  contractHash: string = '0x3b8a60616bde6f6d251e807695900f31ab12ce1a';
  MyContract: any;
  contract: any;
  ABI: any = [{"constant":true,"inputs":[{"name":"idx","type":"uint256"}],"name":"getLocationHistory","outputs":[{"name":"delegate","type":"address"},{"name":"longitude","type":"uint128"},{"name":"latitude","type":"uint128"},{"name":"name","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"recentLocation","outputs":[{"name":"delegate","type":"address"},{"name":"longitude","type":"uint128"},{"name":"latitude","type":"uint128"},{"name":"name","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"longitude","type":"uint128"},{"name":"latitude","type":"uint128"},{"name":"name","type":"bytes32"}],"name":"saveLocation","outputs":[],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"getLastLocation","outputs":[{"components":[{"name":"delegate","type":"address"},{"name":"longitude","type":"uint128"},{"name":"latitude","type":"uint128"},{"name":"name","type":"bytes32"}],"name":"recentLocation","type":"tuple"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"locations","outputs":[{"name":"delegate","type":"address"},{"name":"longitude","type":"uint128"},{"name":"latitude","type":"uint128"},{"name":"name","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"item","outputs":[{"name":"id","type":"bytes32"},{"name":"name","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"id","type":"bytes32"},{"name":"name","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}];


  constructor(private route: ActivatedRoute) { }

  @HostListener('window:load')
  windowLoaded() {
    this.checkAndInstantiateWeb3();
    this.getLocation();
  }

  getLocationHistory() {
    this.MyContract.methods
      .getLocationHistory(0).send({
      'from': '0x902D578B7E7866FaE71b3AB0354C9606631bCe03',
      'gas': '44000'
    }).then((result) => {
      this.MyContract.methods.getLocationHistory(0).call()
        .then(hello => {console.log('hello', hello)});
    });
  }

  private checkAndInstantiateWeb3 = () => {
    if (typeof window.web3 !== 'undefined') {
      console.warn('Using web3 detected from external source.');
      // Use Mist/MetaMask's provider
      this.web3 = new Web3(window.web3.currentProvider);
    } else {
      console.warn(`No web3 detected. Falling back to http://localhost:8545.`);
      this.web3 = new Web3(
        new Web3.providers.HttpProvider('http://localhost:8545')
      );
    }

    this.MyContract = new this.web3.eth.Contract(this.ABI, this.contractHash);
  }

  private getLocation(): void {
    let query = this.route.snapshot.queryParams;

    if (query.action && query.action === 'setLocation') {
      this.setLocation();
    }

  }

  private setLocation(): void {
    navigator.geolocation.getCurrentPosition((position) => {

      this.MyContract.methods.saveLocation(
        position.coords.longitude, position.coords.latitude, window.web3.fromAscii("test")
      ).send({'from': '0x902D578B7E7866FaE71b3AB0354C9606631bCe03'}
      ).then((result) => {
        console.log('saveLocation')
        console.log(result)
      });

      this.getLocationHistory();

    });
  }    

}

Solidity Contract

pragma solidity ^0.4.11;
/// @title QRCodeTracking with delegation.
contract QRCodeTracking {
    struct Location {
        address delegate;
        uint128 longitude;
        uint128 latitude; 
        bytes32 name;
    }

    struct Item {
        bytes32 id;   
        bytes32 name; 
    }

    Item public item;

    Location[] public locations;
    Location public recentLocation;

    function QRCodeTracking(bytes32 id, bytes32 name) public {
        // Limit gas
        locations.length = 100;
        item = Item({id: id, name: name});
    }

    function saveLocation (
        uint128 longitude,
        uint128 latitude,
        bytes32 name
    ) public constant {

        locations.push(Location({
            delegate: msg.sender,
            longitude: longitude,
            latitude: latitude,
            name: name
        }));

    }

    function getLocationHistory(uint idx) constant
        returns (address delegate, uint128 longitude, uint128 latitude, bytes32 name) {

        Location storage loc = locations[idx];

        return (loc.delegate, loc.longitude, loc.latitude, loc.name);
    }

    function getLastLocation() public
        returns (Location recentLocation) {
        recentLocation = locations[locations.length - 1];

        return recentLocation;
    }
}

Solution

  • There's a few issues with your code that center around the use of constant functions and understanding the difference between send and call in web3.

    In Solidity, the constant (or view) modifier is used to mark contract functions that don't change state (see docs). If you attempt to update contract state in a constant function, the state change will not persist. Therefore, in your Solidity contract, saveLocation should NOT be constant. However, both getLastLocation and getLocationHistory only read from state, so those should both be constant.

    On the client side, you have the same distinction when calling a contract function using web3. You use send when you want to execute a transaction on the blockchain, but use call when you want to retrieve data from your contract.

    When you use this.MyContract.methods.getLocationHistory(0).send(), you are executing a transaction against your contract and the transaction receipt is sent back via the callback passed in (or via a .then Promise). Web3 documentation for send can be found here. Important note - nothing from the contract state can be returned from a Solidity function that executes a transaction. If you have returns in a non-constant function, it will compile, but the data is not returned. The only way to return data from the contract is to use constant functions or by using events. For your use case, you want to use this.MyContract.methods.getLocationHistory(0).call(). Call web3 documentation.

    EDIT - Adding simplified test client. Notice the use of send vs call.

    const Web3 = require('web3');
    const solc = require('solc');
    const fs = require('fs');
    
    const provider = new Web3.providers.HttpProvider("http://localhost:8545")
    const web3 = new Web3(provider);
    
    web3.eth.getAccounts().then((accounts) => {
      const code = fs.readFileSync('./QRCodeTracking.sol').toString();
      const compiledCode = solc.compile(code);
    
      const byteCode = compiledCode.contracts[':QRCodeTracking'].bytecode;
      // console.log('byteCode', byteCode);
      const abiDefinition = JSON.parse(compiledCode.contracts[':QRCodeTracking'].interface);
    
      const deployTransactionObject = {
        data: byteCode,
        from: accounts[0],
        gas: 4700000
      };
    
      let deployedContract;
    
      const MyContract = new web3.eth.Contract(abiDefinition, deployTransactionObject);
    
      MyContract.deploy({arguments: [web3.utils.asciiToHex("someId"), web3.utils.asciiToHex("someName")]}).send((err, hash) => {
        if (err)
          console.log("Error: " + err);
        else
          console.log("TX Hash: " + hash);
      }).then(result => {
        deployedContract = result;
        deployedContract.setProvider(provider);
    
        return deployedContract.methods.saveLocation(123456789, 987654321, web3.utils.asciiToHex("newLocationName")).send();
      }).then(saveResult => {
        return deployedContract.methods.getLocationHistory(0).call();
      }).then(locationResult => {
        console.log(locationResult);
      })
    });