1
\$\begingroup\$

I am making a small token project with front-end website to ease the use. The architecture is Ethereum private network with ERC20 contract, PostgreSQL to associate address with alias of wallet, NodeJS and AngularJS.

I am very new to Javascript Promises and I struggled a lot in the past few months to understand how to use them "properly".

What follows is the code for my ExpressJS endpoints. It is working, but could you tell me whether I am doing things properly?

Another question: I would like to move the initialisation of sequelize, web3 and truffle on another js file. I tried with require() but variables become undefined; what should I do?

const express = require('express');
const path = require('path');
const Web3 = require('web3');
const fs = require('fs');
const TruffleContract = require('truffle-contract');
const Sequelize = require('sequelize');
const Aigle = require('aigle');

const router = express.Router();

const Op = Sequelize.Op;
const sequelize = new Sequelize('aircoin', 'giuseppe', '12345', {
  dialect: 'postgres',
  host: "localhost",
  port: 5432,
  operatorsAliases: false
});

let web3;
let contract;

if (typeof web3 !== 'undefined') {
  web3Provider = web3.currentProvider;
} else {
  web3Provider = new Web3.providers.HttpProvider('localhost:8545');
}

web3 = new Web3(web3Provider);

fs.readFile(path.resolve(__dirname, 'TokenERC20.json'), 'UTF-8', function read(err, res) {
  if (err) {
    throw err;
  }

  data = JSON.parse(res);

  contract = new TruffleContract(data);
  contract.setProvider(web3Provider);
});

router.get('/', function(req, res, next) {
  res.sendFile('index.html');
});

router.get('/api/v1/wallet/:ens', function(req, res){
  const name = req.params.ens.replace(" ", "_");

  let result = [];

  const iterator = function(value, num, collection) {
    return contract
      .deployed()
      .then(function(deployed) {
        return deployed.balanceOf(value.address);
      })
      .then(function(data) {
        return data.toNumber() / Math.pow(10, 18);
      })
      .then(function(data) {
        return result.push({ address: value.address, name: name, balance: data });
      })
      .catch(function(err) {
        console.log(err.message);
      });

    return Promise.resolve(null);
  };

  return sequelize.query('SELECT address FROM ens WHERE UPPER(name) LIKE UPPER($1);', { bind: [name], raw: true, type: Sequelize.QueryTypes.SELECT })
    .then(function(row) {
      return Aigle.resolve(row).each(iterator);
    })
    .then(function() {
      return res.json(result);
    })
    .catch(function(err) {
      console.log(err.message);
    });

  //return Promise.resolve(null);
});

router.get('/api/v1/wallet', function(req, res){
  let result = [];

  const iterator = function(value, num, collection) {
    return contract
      .deployed()
      .then(function(deployed) {
        return deployed.balanceOf(value.address);
      })
      .then(function(data) {
        return data.toNumber() / Math.pow(10, 18);
      })
      .then(function(data) {
        return result.push({ address: value.address, name: value.name, balance: data });
      })
      .catch(function(err) {
        console.log(err.message);
      });

    return Promise.resolve(null);
  };

  return sequelize.query('SELECT address, name FROM ens;', { raw: true, type: Sequelize.QueryTypes.SELECT })
    .then(function(row) {
      return Aigle.resolve(row).each(iterator);
    })
    .then(function() {
      return res.json(result);
    })
    .catch(function(err) {
      console.log(err.message);
    });

  //return Promise.resolve(null);
});

router.post('/api/v1/wallet', function(req, res){
  const data = { name: req.body.name.replace(" ", "_"), passphrase: req.body.passphrase };

  const address = web3.personal.newAccount(data.passphrase);
  const bank = "0x49b36fa1772bdb4d9249f43179832ccdb3732ffc";

  web3.personal.unlockAccount(bank, "", function(error, res) {
    if (error) {
      console.log(error.message);
      return;
    }

    var amount = web3.toWei(3.20, "ether")
    web3.eth.sendTransaction({ from: bank, to: address, value: amount })
  });

  return sequelize.query('INSERT INTO ens (address, name) VALUES ($1, $2);', { bind: [address, data.name], raw: true, type: Sequelize.QueryTypes.INSERT })
    .then(function() {
      return res.status(200).json({ address: address });
    });

  //return Promise.resolve(null);
});

router.post('/api/v1/transaction', function(req, res){
  const data = { from: req.body.from.replace(" ", "_"), passphrase: req.body.passphrase, to: req.body.to.replace(" ", "_"), amount: req.body.amount*10**18 };

  let result = {};

  const iterator = function(value, num, collection) {
    return result[value.name] = value.address;
  };

  return sequelize.query('SELECT address, name FROM ens WHERE UPPER(name) LIKE UPPER($1) OR UPPER(name) LIKE UPPER($2);', { bind: [data.from, data.to], raw: true, type: Sequelize.QueryTypes.SELECT })
    .then(function(row) {
      return Aigle.resolve(row).each(iterator);
    })
    .then(function() {
      return web3.personal.unlockAccount(result[data.from], data.passphrase)
    })
    .then(function() {
      return contract.deployed()
    })
    .then(function(deployed) {
      return deployed.transfer(result[data.to], data.amount, { from: result[data.from] });
    })
    .then(function(tx) {
      console.log(tx);
      return res.status(200).json(tx);
    })
    .catch(function(err) {
      console.log(err.message);
      return res.status(500).json(err);
    });

  //return Promise.resolve(null);
});

module.exports = router;
\$\endgroup\$
1

1 Answer 1

1
\$\begingroup\$

Code doesn't look too bad for a first attempt at using promises.

Here are a few observations.

General

  1. A wholly synchronous step in a promise chain doesn't need its own .then() and can be merged downwards with following synchronous step(s) up to and including the next asynchronous step.
  2. Completely flat promise chains are not always the best. Nesting is perfectly acceptable and often offers big benefits in terms of closure and flow control.
  3. You can avoid clumsy outer vars when it's possible to pass them down a promise chain or keep them in a closure.
  4. Arrow functions would help make the code more concise (and arguably more readable).

Specific

  1. The routes take no account of failure of fs.readFile(path.resolve(__dirname, 'TokenERC20.json'), 'UTF-8').
  2. In fixing (1), you can (probably) execute contract.deployed() once and deliver deployed via a deployedPromise promise in those routes that use it.
  3. In routes that use deployed, you can start with deployedPromise.then(...) to ensure that nothing is executed if fs.readFile(...) or contract.deployed() failed.
  4. In router.post('/api/v1/wallet'), sequelize.query(...) doesn't wait for web3.personal.unlockAccount().
  5. The test if (typeof web3 !== 'undefined') is a bit odd when web3 is guaranteed to be undefined.

With all that in mind, I end up with the following :

const express = require('express');
const path = require('path');
const Web3 = require('web3');
const fs = require('fs');
const TruffleContract = require('truffle-contract');
const Sequelize = require('sequelize');
const Aigle = require('aigle');

const router = express.Router();
// const Op = Sequelize.Op; // not used

const sequelize = new Sequelize('aircoin', 'giuseppe', '12345', {
    'dialect': 'postgres',
    'host': 'localhost',
    'port': 5432,
    'operatorsAliases': false
});

let web3;

if (typeof web3 !== 'undefined') { // ???
    web3Provider = web3.currentProvider;
} else {
    web3Provider = new Web3.providers.HttpProvider('localhost:8545');
}

web3 = new Web3(web3Provider);

let deployedPromise = Aigle.promisify(fs.readFile)(path.resolve(__dirname, 'TokenERC20.json'), 'UTF-8')
.then(res => {
    let contract = new TruffleContract(JSON.parse(res));
    contract.setProvider(web3Provider);
    return contract.deployed();
});

router.get('/', function(req, res, next) {
    res.sendFile('index.html');
});

router.get('/api/v1/wallet/:ens', function(req, res) {
    return deployedPromise
    .then(deployed => {
        const name = req.params.ens.replace(' ', '_');
        return sequelize.query('SELECT address FROM ens WHERE UPPER(name) LIKE UPPER($1);', { 'bind': [name], 'raw': true, 'type': Sequelize.QueryTypes.SELECT })
        .then(row => {
            return Aigle.resolve(row).map(value => {
                return deployed.balanceOf(value.address)
                .then(data => { 'address': value.address, 'name': name, 'balance': data.toNumber() / Math.pow(10, 18) });
            });
        });
    })
    .then(result => {
        res.json(result);
    })
    .catch(err => {
        console.log(err.message);
    });
});

router.get('/api/v1/wallet', function(req, res) {
    return deployedPromise
    .then(deployed => {
        return sequelize.query('SELECT address, name FROM ens;', { 'raw': true, 'type': Sequelize.QueryTypes.SELECT })
        .then(row => {
            return Aigle.resolve(row).map(value => {
                return deployed.balanceOf(value.address)
                .then(data => { 'address': value.address, 'name': value.name, 'balance': data.toNumber() / Math.pow(10, 18) })
            });
        });
    })
    .then(result => {
        res.json(result);
    })
    .catch(error => {
        console.log(err.message);
    });
});
router.post('/api/v1/wallet', function(req, res) {
    const data = { 'name': req.body.name.replace(' ', '_'), 'passphrase': req.body.passphrase };
    const address = web3.personal.newAccount(data.passphrase);
    const bank = "0x49b36fa1772bdb4d9249f43179832ccdb3732ffc";
    return web3.personal.unlockAccount(bank, '')
    .then(() => web3.eth.sendTransaction({ 'from': bank, 'to': address, 'value': web3.toWei(3.20, 'ether') }))
    .then(() => sequelize.query('INSERT INTO ens (address, name) VALUES ($1, $2);', { 'bind': [address, data.name], 'raw': true, 'type': Sequelize.QueryTypes.INSERT }))
    .then(() => {
        res.status(200).json({ 'address': address });
    })
    .catch(error => {
        console.log(error.message);
    });
});

router.post('/api/v1/transaction', function(req, res) {
    return deployedPromise
    .then(deployed => {
        const data = { 'from': req.body.from.replace(' ', '_'), 'passphrase': req.body.passphrase, 'to': req.body.to.replace(' ', '_'), 'amount': req.body.amount*10**18 };
        return sequelize.query('SELECT address, name FROM ens WHERE UPPER(name) LIKE UPPER($1) OR UPPER(name) LIKE UPPER($2);', { 'bind': [data.from, data.to], 'raw': true, 'type': Sequelize.QueryTypes.SELECT })
        .then(row => {
            let result = {};
            return Aigle.resolve(row).each(value => {
                result[value.name] = value.address;
            })
            .then(() => {
                return web3.personal.unlockAccount(result[data.from], data.passphrase)
                .then(deployed => deployed.transfer(result[data.to], data.amount, { 'from': result[data.from] }));
            });
        });
    })
    .then(tx => {
        res.status(200).json(tx);
    })
    .catch(error => {
        res.status(500).json(error);
    });
});
module.exports = router;

Untested and includes a few assumptions, therefore may not be 100% correct. Be prepred to debug, or just raid for ideas.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.