forked from startup-class/bitstarter-leaderboard
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit c755156
Showing
26 changed files
with
1,981 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
localhost:5432:bitdb0:ubuntu:bitpass0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
web: node web.js |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
}); | ||
}; |
Oops, something went wrong.