I didn't make it to the next stage in this interview process, because another applicant did better than me, which wasn't surprising given that this was my first foray into Node.js.
I'd like some tips on how I could have made this a better submission, and also more generally I'd like to know how I could do things more idiomatically.
Here are the instructions I was given:
Using node, an http server of your choice (express recommended) and a DB of your choice (mongo or postgres recommended), make a web app. It will have at least three functions. These functions may be written as a standard JSON API, REST API, or GraphQL endpoints.
- Accept an Ethereum address as input, and then:
- query https://etherscan.io/apis and collects all transactions associated with that address.
- store the transactions in the DB.
- store the address balances in the DB.
- Return transactions of stored ETH address, and accept some form of search params (which params are up to you).
- Return stored address balances by ETH address, and any other information about the address you see fit.
Be prepared to explain design choices that you made. Frameworks, libraries, directory structures, coding contentions, et al.
And below are the files I submitted. I've pre-pended each code segment with its file name, and all files are in the same top-level folder.
index.js:
const express = require('express')
const app = express()
const etherscan = require('etherscan-api').init('(my api key redacted)')
const { Client } = require('pg')
const postgres = new Client()
postgres.connect().catch((error) => { console.log('connecting to postgres: ' + error) })
const functions = require('./functions.js')
app.put('/ethtrancache/:address(0x[0-9a-fA-F]{40})/', handleAddAddress)
app.get('/ethtrancache/:address(0x[0-9a-fA-F]{40})/', handleGetBalance)
app.delete('/ethtrancache/:address(0x[0-9a-fA-F]{40})/', handleDeleteAddress)
app.get('/ethtrancache/:address(0x[0-9a-fA-F]{40})/transactions', handleGetTransactions)
async function handleDeleteAddress (request, response) {
try {
await functions.deleteAddress(postgres, request.params['address'])
} catch (err) {
response.send(
{ result: 'failure', msg: 'error deleting existing address: ' + err })
return
}
response.send({result: 'success'})
}
async function handleAddAddress (request, response) {
try {
await functions.addAddress(postgres, etherscan, request.params['address'])
} catch (err) {
response.send({ result: 'failure', msg: 'error adding address: ' + err })
return
}
response.send({ result: 'success' })
}
async function handleGetBalance (request, response) {
var balance
try {
balance = await functions.getBalance(postgres, request.params['address'])
} catch (err) {
response.send({ result: 'failure', msg: 'error getting balance: ' + err })
return
}
response.send({ result: 'success', balance: balance })
}
async function handleGetTransactions (request, response) {
var transactions
try {
transactions = await functions.getTransactions(postgres, request.params['address'], request.query.other_address)
} catch (err) {
response.send({ result: 'failure', msg: 'error getting transactions: ' + err })
return
}
response.send({ result: 'success', transactions: transactions })
}
app.listen(3000, () => console.log('listening on port 3000'))
functions.js:
async function addAddress (postgres, etherscan, address) {
/*
"Accept an Ethereum address as input, and then:
1. query https://etherscan.io/apis and collects all transactions
associated with that address.
1. store the transactions in the DB.
2. store the address balance in the DB."
*/
// clear out existing balance and transactions
try {
await deleteAddress(postgres, address)
} catch (err) { throw new Error('error deleting existing address: ' + err) }
/* scrape and store eth balance */
var balance
try {
balance = await etherscan.account.balance(address)
} catch (err) { throw new Error('err getting eth balance: ' + err) }
try {
await postgres.query(
'INSERT INTO eth_balances(address, balance) VALUES(LOWER($1), $2)',
[address, balance['result']])
} catch (err) { throw new Error('error storing eth balance: ' + err) }
/* scrape and store transactions */
var txlist
try {
txlist = await etherscan.account.txlist(address)
} catch (err) { throw new Error('error getting transactions: ' + err) }
try {
for (var i = 0; i < txlist.result.length; i++) {
await postgres.query(
'INSERT INTO transactions(to_address, txn_id, from_address, value)' +
' VALUES(LOWER($1), LOWER($2), LOWER($3), $4)',
[ txlist.result[i].to,
txlist.result[i].hash,
txlist.result[i].from,
txlist.result[i].value ])
}
} catch (err) { throw new Error('error storing transactions: ' + err) }
}
async function deleteAddress (postgres, address) {
try {
await postgres.query(
'DELETE FROM eth_balances WHERE address = LOWER($1)',
[address])
await postgres.query(
'DELETE FROM transactions WHERE from_address = LOWER($1) OR to_address = LOWER($1)',
[address])
} catch (err) { throw new Error('PostgreSQL error: ' + err) }
}
async function getBalance (postgres, address) {
/*
Return stored address balance by ETH address
*/
try {
var result = await postgres.query(
'SELECT balance FROM eth_balances WHERE address = LOWER($1)',
[address])
if (result.rows.length === 0) { throw new Error('no such address') }
return result.rows[0].balance
} catch (err) { throw new Error('error getting eth_balance: ' + err) }
}
async function getTransactions (postgres, address, otherAddress) {
/*
Return transactions of stored ETH address, and accept some form of search
params (which params are up to you).
*/
var query =
'SELECT * FROM transactions WHERE ' +
'from_address = LOWER($1) OR to_address = LOWER($1)'
var values = [address]
if (otherAddress !== undefined) {
query += ' AND ( from_address = LOWER($2) OR to_address = LOWER($2) )'
values.push(otherAddress)
}
query += ';'
try {
var result = await postgres.query(query, values)
} catch (err) { throw new Error('error getting transactions: ' + err) }
return result.rows
}
module.exports = {
addAddress: addAddress,
deleteAddress: deleteAddress,
getBalance: getBalance,
getTransactions: getTransactions
}
package.json:
{
"name": "assignment",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"lint": "node_modules/.bin/eslint *.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"etherscan-api": "^8.0.4",
"express": "^4.16.3",
"pg": "^7.4.3"
},
"devDependencies": {
"eslint": "^4.19.1",
"eslint-config-standard": "^11.0.0",
"eslint-plugin-import": "^2.11.0",
"eslint-plugin-node": "^6.0.1",
"eslint-plugin-promise": "^3.7.0",
"eslint-plugin-standard": "^3.1.0"
}
}
initDatabase.sh:
#!/bin/sh
# tested with PostgreSQL version 9.3.22
. ./pgenv
dropdb -h $PGHOST -U $PGUSER $PGDATABASE || exit 1
createdb -h $PGHOST -U $PGUSER $PGDATABASE || exit 1
psql -h $PGHOST -U $PGUSER $PGDATABASE -f createTables.psql
pgenv: (This I filled in with the credentials for a PostgreSQL instance I spun up on AWS.)
export PGHOST=
export PGUSER=
export PGPASSWORD=
export PGDATABASE=assignment
createTables.psql:
create table eth_balances(address text PRIMARY KEY, balance bigint);
create table transactions(to_address text, txn_id text UNIQUE, from_address text, value bigint);