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

1

6

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;
    }
}
Shiloh answered 3/2, 2018 at 23:14 Comment(1)
Problem occur when you call this.MyContract.methods.saveLocation?Gaius
B
4

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);
  })
});
Br answered 4/2, 2018 at 0:14 Comment(5)
Are you saying .call() will return the data? .call() only returns locally, it doesn't access the blockchain. –Shiloh
That's not correct. It doesn't publish to the blockchain, but it executes within the EVM and does indeed read from it. If you're running a local node, then the invocation runs locally and you don't pay for the gas used.Br
Ok, but it still doesn't return my data, which is my original question.Shiloh
Just noticed the other bug you have: locations.length = 100; is creating 100 elements in your locations array all initialized with fields set to 0. When you call saveLocation, you're adding an element to your array at index 100. When you retrieve the element at index 0, you're getting an all 0 location object. Remove that line. After making that change (plus the others I mentioned), I tested the contract and it ran. I updated the answer with my own version of the client that's a little more straightforward.Br
Ok, great thanks. I initially put that in because Remix IDE wouldn't compile and found a stackoverflow post that said to set the length to avoid the infinite recursion error that Remix was throwing. Glad I lost three days of work over a Remix IDE bug. Now it works in Remix no problem. Ugh..Shiloh

© 2022 - 2024 — McMap. All rights reserved.