Skip to content

Commit

Permalink
Imported files.
Browse files Browse the repository at this point in the history
  • Loading branch information
balajis committed Sep 7, 2013
0 parents commit c755156
Show file tree
Hide file tree
Showing 26 changed files with 1,981 additions and 0 deletions.
16 changes: 16 additions & 0 deletions .env.dummy
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# This file helps keep sensitive API keys out of your public git repository.
# Details: https://devcenter.heroku.com/articles/config-vars
#
# Towards this end, please do the following:
#
# 1) Get your Coinbase key from coinbase.com/account/integrations
# 2) Edit the variable below
# 3) cp this file into bitstarter/.env and delete these comments
# $ cp .env.dummy .env
# 4) Now you can do
# $ foreman start
# to run the server locally (and read from these env variables), or
# $ git push heroku master; heroku config:push
# to push the .env file remotely.
COINBASE_API_KEY=this-is-a-dummy-api-key
PORT=8080
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Heroku environmental variables, including API keys.
# https://devcenter.heroku.com/articles/config-vars
.env

# npm packages
node_modules

# Mac
.DS_Store

# Emacs
*~

# Log file
bitstarter-leaderboard.log
1 change: 1 addition & 0 deletions .pgpass
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
localhost:5432:bitdb0:ubuntu:bitpass0
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: node web.js
614 changes: 614 additions & 0 deletions README.md

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
Note that COINBASE_PREORDER_DATA_CODE is the button code from
coinbase.com/merchant_tools, used for the Preorder Button. This is
different from the COINBASE_API key in .env file, which is from
coinbase.com/account/integrations
The button code can be shown publicly, while the API key should only be
included in a .env file and never shown publicly. The former allows
people to send you money, the latter allows people to send money from
your account.
For FUNDING_UNIT_SYMBOL, we use mBTC to represent 1/1000 of a Bitcoin and
FUNDING_SI_SCALE for the corresponding multiplier.
Note that for FUNDING_UNIT_SYMBOL, in theory we could use the Thai Baht
symbol, but then we'd have to change the font. If you use another
payment backend, you can substitute "$" for the dollar or use one of the
other currency symbols.
- https://en.bitcoin.it/wiki/Bitcoin_symbol#Existing_Unicode_symbol
- http://webdesign.about.com/od/localization/l/blhtmlcodes-cur.htm#codes
*/
var Constants = {
APP_NAME: "Bitstarter",
FUNDING_TARGET: 10.00,
FUNDING_UNIT_SYMBOL: "mBTC",
FUNDING_SI_SCALE: 1000,
FUNDING_END_DATE: new Date("September 8, 2013"),
PRODUCT_NAME: "Product: Development Version",
PRODUCT_SHORT_DESCRIPTION: "One sentence description.",
TWITTER_USERNAME: "nodejs",
TWITTER_TWEET: "This student crowdfunder looks interesting.",
COINBASE_PREORDER_DATA_CODE: "13b56883764b54e6ab56fef3bcc7229c",
days_left: function() {
return Math.max(Math.ceil((this.FUNDING_END_DATE - new Date()) / (1000*60*60*24)), 0);
}
};

module.exports = Constants;
85 changes: 85 additions & 0 deletions models/coinbase.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
The Coinbase API limits the number of orders that can be mirrored at a
time to 25, with subsequent orders on new pages.
The following code hits the API once to determine the number of pages,
and then uses this input to set up an async.mapLimit that pulls
the order data and merges it together.
Note that there are several improvements possible here:
- You can add much more sophisticated error handling for each Coinbase
API call, along with retries for fails, delays between requests, and
the like.
- You can make each Coinbase API call write directly to the database,
rather than aggregating them and writing in one block. Depending
on what you want to do, this might be preferable.
- If you have a very large number of orders, you might have an issue
with the default Heroku deployment process, which requires a port to
be bound within 60 seconds of deployment. In this case you might not
be able to do database update and deploy in one step and would have to
revisit how web.js is set up; for example you might only download as
many orders as you can get in the first 60 seconds, and then have the
rest downloaded after the app boots. Or you might mirror all the
Coinbase data offline and have the database separate from the main
app.
Overall, though, this is another good illustration of using async.compose
to manage asynchrony.
*/
var async = require('async');
var request = require('request');
var uu = require('underscore');

var coinbase_api_url = function(page) {
return "https://coinbase.com/api/v1/orders?page=" +
page.toString() + "&api_key=" + process.env.COINBASE_API_KEY;
};

var get_ncoinbase_page = function(init, cb) {
request.get(coinbase_api_url(init), function(err, resp, body) {
var orders_json = JSON.parse(body);
console.log("Finished get_ncoinbase_page");
cb(null, orders_json.num_pages);
});
};

var ncoinbase_page2coinbase_json = function(npage, cb) {
console.log("Starting ncoinbase_page2coinbase_json with npage = " + npage);
var inds = uu.range(1, npage + 1);
var LIMIT = 5;
var getjson = function(item, cb2) {
request.get(coinbase_api_url(item), function(err, resp, body) {
var orders_json = JSON.parse(body);
console.log("Finished API request for Coinbase Order Page " + item);
cb2(null, orders_json.orders);
});
};
async.mapLimit(inds, LIMIT, getjson, function(err, results) {
cb(null, uu.flatten(results));
});
};

var get_coinbase_json = async.compose(ncoinbase_page2coinbase_json,
get_ncoinbase_page);
/*
Example of API use.
The 1 argument to get_coinbase_json is used to specify that page=1
is requested and parsed to get the num_pages. That is, we create
a URL of the form:
https://coinbase.com/api/v1/orders?page=1&api_key=YOUR-COINBASE-API-KEY
...and parse the num_pages field from it first.
*/
var debug_get_coinbase_json = function() {
get_coinbase_json(1, function(err, result) {
console.log(result);
});
};

module.exports = { 'get_coinbase_json': get_coinbase_json,
'debug_get_coinbase_json': debug_get_coinbase_json};
53 changes: 53 additions & 0 deletions models/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
if (!global.hasOwnProperty('db')) {
var Sequelize = require('sequelize');
var sq = null;
var fs = require('fs');
var path = require('path');
var PGPASS_FILE = path.join(__dirname, '../.pgpass');
if (process.env.DATABASE_URL) {
/* Remote database
Do `heroku config` for details. We will be parsing a connection
string of the form:
postgres://bucsqywelrjenr:ffGhjpe9dR13uL7anYjuk3qzXo@\
ec2-54-221-204-17.compute-1.amazonaws.com:5432/d4cftmgjmremg1
*/
var pgregex = /postgres:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/;
var match = process.env.DATABASE_URL.match(pgregex);
var user = match[1];
var password = match[2];
var host = match[3];
var port = match[4];
var dbname = match[5];
var config = {
dialect: 'postgres',
protocol: 'postgres',
port: port,
host: host,
logging: true //false
};
sq = new Sequelize(dbname, user, password, config);
} else {
/* Local database
We parse the .pgpass file for the connection string parameters.
*/
var pgtokens = fs.readFileSync(PGPASS_FILE).toString().trimRight().split(':');
var host = pgtokens[0];
var port = pgtokens[1];
var dbname = pgtokens[2];
var user = pgtokens[3];
var password = pgtokens[4];
var config = {
dialect: 'postgres',
protocol: 'postgres',
port: port,
host: host,
};
var sq = new Sequelize(dbname, user, password, config);
}
global.db = {
Sequelize: Sequelize,
sequelize: sq,
Order: sq.import(__dirname + '/order')
};
}
module.exports = global.db;
162 changes: 162 additions & 0 deletions models/order.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
Object/Relational mapping for instances of the Order class.
- classes correspond to tables
- instances correspond to rows
- fields correspond to columns
In other words, this code defines how a row in the PostgreSQL "Order"
table maps to the JS Order object. Note that we've omitted a fair bit of
error handling from the classMethods and instanceMethods for simplicity.
*/
var async = require('async');
var util = require('util');
var uu = require('underscore');
var coinbase = require('./coinbase');

module.exports = function(sequelize, DataTypes) {
return sequelize.define("Order", {
coinbase_id: {type: DataTypes.STRING, unique: true, allowNull: false},
amount: {type: DataTypes.FLOAT},
time: {type: DataTypes.STRING, allowNull: false}
}, {
classMethods: {
numOrders: function() {
this.count().success(function(c) {
console.log("There are %s Orders", c);});
},
allToJSON: function(successcb, errcb) {
this.findAll()
.success(function(orders) {
successcb(uu.invoke(orders, 'toJSON'));
})
.error(errcb);
},
totals: function(successcb, errcb) {
this.findAll().success(function(orders) {
var total_funded = 0.0;
orders.forEach(function(order) {
total_funded += parseFloat(order.amount);
});
var totals = {total_funded: total_funded,
num_orders: orders.length};
successcb(totals);
}).error(errcb);
},
addAllFromJSON: function(orders, errcb) {
/*
This method is implemented naively and can be slow if
you have many orders.
The ideal solution would first determine in bulk which of the
potentially new orders in order_json is actually new (and not
stored in the database). One way to do this is via the NOT IN
operator, which calculates a set difference:
http://www.postgresql.org/docs/9.1/static/functions-comparisons.html
This should work for even a large set of orders in the NOT IN
clause (http://stackoverflow.com/a/3407914) but you may need
to profile the query further.
Once you have the list of new orders (i.e. orders which are
in Coinbase but not locally stored in the database), then
you'd want to launch several concurrent addFromJSON calls
using async.eachLimit
(https://github.com/caolan/async#eachLimit). The exact value
of the limit is how many concurrent reads and writes your
Postgres installation can handle. This is outside the scope
of the class and depends on your Postgres database settings,
the tuning of your EC2 instance, and other parameters. For a
t1.micro, we just set this to 1 to prevent the system from
hanging.
*/
var MAX_CONCURRENT_POSTGRES_QUERIES = 1;
async.eachLimit(orders,
MAX_CONCURRENT_POSTGRES_QUERIES,
this.addFromJSON.bind(this), errcb);
},
addFromJSON: function(order_obj, cb) {
/*
Add from JSON only if order has not already been added to
our database.
Note the tricky use of var _Order. We use this to pass in
the Order class to the success callback, as 'this' within
the scope of the callback is redefined to not be the Order
class but rather an individual Order instance.
Put another way: within this classmethod, 'this' is
'Order'. But within the callback of Order.find, 'this'
corresponds to the individual instance. We could also
do something where we accessed the class to which an instance
belongs, but this method is a bit more clear.
*/
var order = order_obj.order; // order json from coinbase
if (order.status != "completed") {
cb();
} else {
var _Order = this;
_Order.find({where: {coinbase_id: order.id}}).success(function(order_instance) {
if (order_instance) {
// order already exists, do nothing
cb();
} else {
/*
Build instance and save.
Uses the _Order from the enclosing scope,
as 'this' within the callback refers to the current
found instance.
Note also that for the amount, we convert
satoshis (the smallest Bitcoin denomination,
corresponding to 1e-8 BTC, aka 'Bitcents') to
BTC.
*/
var new_order_instance = _Order.build({
coinbase_id: order.id,
amount: order.total_btc.cents / 100000000,
time: order.created_at
});
new_order_instance.save().success(function() {
cb();
}).error(function(err) {
cb(err);
});
}
});
}
},
refreshFromCoinbase: function(cb) {
/*
This function hits Coinbase to download the latest list of
orders and then mirrors them to the local database. The
callback passed in expects a single error argument,
cb(err). Note that one can add much more error handling
here; we've removed that for the sake of clarity.
*/
var _Order = this;
coinbase.get_coinbase_json(1, function(err, orders) {
_Order.addAllFromJSON(orders, cb);
});
}
},
instanceMethods: {
repr: function() {
return util.format(
"Order <ID: %s Coinbase_ID:%s Amount:%s Time:%s " +
"Created: %s Updated:%s", this.id, this.coinbase_id,
this.amount, this.time, this.createdAt, this.updatedAt);
},
amountInUSD: function() {
/*
Illustrative only.
For a real app we'd want to periodically pull down and cache
the value from http://blockchain.info/ticker.
*/
var BTC2USD = 118.86;
return this.amount * BTC2USD;
}
}
});
};
Loading

0 comments on commit c755156

Please sign in to comment.