From 27c6e8f89278567f6833c20ec1740c6d695819a6 Mon Sep 17 00:00:00 2001
From: yamadapc <tacla.yamada@gmail.com>
Date: Mon, 3 Oct 2016 09:09:08 -0300
Subject: [PATCH] Refactor the API

- Add webpack build system
- Send messages in queue
- Assert that event listeners are cleaned-up (no memory-leaks)
- Expose `Async` suffixed promise versions of the API
- Use promises internally
- Add tests
---
 .babelrc                |    1 +
 .gitignore              |    1 +
 lib/api-from-methods.js |   61 --
 lib/api.js              |  366 ++++++++----
 lib/broadcast.js        | 1169 ++++++++++++++++++++-------------------
 lib/browser.js          |    2 +-
 lib/formatter.js        |   63 ++-
 lib/util.js             |    6 +
 package.json            |   16 +-
 test/api.test.js        |  139 ++++-
 test/test-post.json     |   13 +
 test/test.html          |   13 +
 webpack.config.js       |    2 +
 webpack/makeConfig.js   |  105 ++++
 14 files changed, 1159 insertions(+), 798 deletions(-)
 delete mode 100644 lib/api-from-methods.js
 create mode 100644 lib/util.js
 create mode 100644 test/test-post.json
 create mode 100644 test/test.html
 create mode 100644 webpack.config.js
 create mode 100644 webpack/makeConfig.js

diff --git a/.babelrc b/.babelrc
index 6b6a9ee..d70cb40 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,5 +1,6 @@
 {
   "presets": [
+    "es2015",
     "es2017"
   ]
 }
diff --git a/.gitignore b/.gitignore
index f484e03..812a80e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,4 @@ build/Release
 # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
 node_modules
 .tern-port
+dist
diff --git a/lib/api-from-methods.js b/lib/api-from-methods.js
deleted file mode 100644
index 8ce00d7..0000000
--- a/lib/api-from-methods.js
+++ /dev/null
@@ -1,61 +0,0 @@
-var methods = require('./methods.json');
-
-var snakeCaseRe = /_([a-z])/g
-function camelCase(str) {
-  return str.replace(snakeCaseRe, function (_m, l) {
-    return l.toUpperCase();
-  });
-}
-
-exports = module.exports = function generateMethods(Steem) {
-  methods.reduce(function (memo, method) {
-    var methodName = camelCase(method.method);
-
-    memo[methodName + 'With'] =
-      function Steem$specializedSendWith(options, callback) {
-        var params = method.params.map(function (param) {
-          return options[param];
-        });
-        var iterator = Steem.iterate();
-
-        return Steem.send(method.api, {
-          id: iterator,
-          method: method.method,
-          params: params,
-        }, function (err, data) {
-          if (err) return callback(err);
-          if (data && data.id === iterator) return callback(err, data.result);
-          // TODO - Do something here
-        });
-      };
-
-    memo[methodName] =
-      function Steem$specializedSend() {
-        var args = arguments;
-        var options = method.params.reduce(function (memo, param, i) {
-          memo[param] = args[i];
-          return memo;
-        }, {});
-        var callback = args[method.params.length];
-        memo[methodName + 'With'](options, callback);
-      };
-
-    return memo;
-  }, Steem);
-};
-
-/*
-
-console.log(exports);
-
-exports.getBlockWith({
-  blockNum: 1,
-}, function (err, operation) {
-  console.log(err, operation);
-});
-
-exports.getBlock(1, function (err, operation) {
-  console.log(err, operation);
-});
-
- */
diff --git a/lib/api.js b/lib/api.js
index 131aaed..7fe31bd 100644
--- a/lib/api.js
+++ b/lib/api.js
@@ -1,151 +1,273 @@
-var isNode = require('detect-node');
-if (isNode) var WS = require('ws');
+import Debug from 'debug';
+import EventEmitter from 'events';
+import Promise from 'bluebird';
+import isNode from 'detect-node';
 
-var Steem = {
+import methods from './methods';
+import {camelCase} from './util';
+
+const debugEmitters = Debug('steemjs:emitters');
+const debugProtocol = Debug('steemjs:protocol');
+const debugSetup = Debug('steemjs:setup');
+const debugWs = Debug('steemjs:ws');
+
+let WebSocket;
+if (isNode) {
+  WebSocket = require('ws'); // eslint-disable-line global-require
+} else if (typeof window !== 'undefined') {
+  WebSocket = window.WebSocket;
+} else {
+  throw new Error('Couldn\'t decide on a `WebSocket` class');
+}
+
+const DEFAULTS = {
   url: 'wss://steemit.com/wspa',
   apiIds: {
-    'database_api': 0,
-    'login_api': 1,
-    'follow_api': 2,
-    'network_broadcast_api': 4
+    database_api: 0,
+    login_api: 1,
+    follow_api: 2,
+    network_broadcast_api: 4
   },
   id: 0,
-  reqs: [],
-  isOpen: false,
-  isReady: false
 };
 
-Steem.setWebSocket = function(url) {
-  this.url = url;
-};
+export class Steem extends EventEmitter {
+  constructor(options = {}) {
+    super(options);
+    Object.assign(options, DEFAULTS);
+    this.options = options;
 
-Steem.init = function(callback) {
-  if (!this.isReady) {
-    if (isNode) {
-      this.ws = new WS(this.url);
-      this.ws.setMaxListeners(0);
-    } else {
-      this.ws = new WebSocket(this.url);
-    }
-    this.ws.addEventListener('close', function() {
-      this.ws.close();
-      this.isReady = false;
-      this.isOpen = false;
-    }.bind(this));
-    this.isReady = true;
+    this.id = 0;
+    this.currentP = Promise.fulfilled();
+    this.apiIds = this.options.apiIds;
+    this.isOpen = false;
+    this.start();
   }
-  if (!this.isOpen) {
-    this.ws.addEventListener('open', function() {
-      this.isOpen = true;
-      this.getApiByName('database_api', function() {});
-      this.getApiByName('login_api', function() {});
-      this.getApiByName('follow_api', function() {});
-      this.getApiByName('network_broadcast_api', function() {});
-      callback();
-    }.bind(this));
-  } else {
-    callback();
+
+  start() {
+    this.startP = new Promise((resolve /* , reject*/) => {
+      this.ws = new WebSocket(this.options.url);
+      this.releases = [
+        this.listenTo(this.ws, 'open', () => {
+          debugWs('Opened WS connection with', this.options.url);
+          this.isOpen = true;
+          resolve();
+        }),
+        this.listenTo(this.ws, 'close', () => {
+          debugWs('Closed WS connection with', this.options.url);
+          this.isOpen = false;
+        }),
+        this.listenTo(this.ws, 'message', (message) => {
+          debugWs('Received message', message.data);
+          this.emit('message', JSON.parse(message.data));
+        }),
+      ];
+    });
+    this.apiIdsP = this.getApiIds();
+    return this.startP;
   }
-};
 
-Steem.iterate = function() {
-  this.id++;
-  var id = this.id;
-  this.reqs.push(id);
-  return id;
-};
+  stop() {
+    this.releases.forEach((release) => release());
+    this.ws.removeEventListener();
+    this.ws.close();
+    delete this.ws;
+    delete this.releases;
+  }
+
+  listenTo(target, eventName, callback) {
+    debugEmitters('Adding listener for', eventName, 'from', target.constructor.name);
+    if (target.addEventListener) target.addEventListener(eventName, callback);
+    else target.on(eventName, callback);
 
-Steem.getApi = function(api, callback) {
-  if (this.apiIds[api] || this.apiIds[api] === 0) {
-    callback('', this.apiIds[api]);
-  } else {
-    this.getApiByName(api, function(err, result) {
-      this.apiIds[api] = result;
-      callback('', result);
-    }.bind(this));
+    return () => {
+      debugEmitters('Removing listener for', eventName, 'from', target.constructor.name);
+      if (target.removeEventListener) target.removeEventListener(eventName, callback);
+      else target.removeListener(eventName, callback);
+    };
   }
-};
 
-Steem.send = function(api, data, callback) {
-  data.id = data.id || 0;
-  data.params = data.params || [];
-  this.init(function(){
-    var call = {};
-    call.id = data.id;
-    call.method = 'call';
-    call.params = [this.apiIds[api], data.method, data.params];
-    this.ws.send(JSON.stringify(call));
-  }.bind(this));
-
-  this.ws.addEventListener('message', function(msg) {
-    var data = JSON.parse(msg.data);
-    var err = (data.error && data.error.data && data.error.data.stack)? data.error.data.stack : '';
-    callback(err, data);
-  }.bind(this));
-
-  this.ws.addEventListener('error', function(error){
-    callback(error, null);
-  });
-};
+  getApiIds() {
+    return Promise.map(Object.keys(this.apiIds), (name) => {
+      debugSetup('Syncing API IDs', name);
+      return this.getApiByNameAsync(name).then((result) => {
+        this.apiIds[name] = result;
+      });
+    });
+  }
 
+  send(api, data, callback) {
+    const id = data.id || this.id++;
+    const currentP = this.currentP;
+    this.currentP = Promise.join(this.startP, currentP)
+      .then(() => new Promise((resolve, reject) => {
+        const payload = JSON.stringify({
+          id,
+          method: 'call',
+          params: [
+            this.apiIds[api],
+            data.method,
+            data.params,
+          ],
+        });
+
+        const release = this.listenTo(this, 'message', (message) => {
+          // We're still seeing old messages
+          if (message.id < id) {
+            debugProtocol('Old message was dropped', message);
+            return;
+          }
+
+          release();
+
+          // We dropped a message
+          if (message.id !== id) {
+            debugProtocol('Response to RPC call was dropped', payload);
+            return;
+          }
+
+          // Our message's response came back
+          const errorCause = data.error;
+          if (errorCause) {
+            const err = new Error(errorCause);
+            err.message = data;
+            reject(err);
+            return;
+          }
+
+          debugProtocol('Resolved', id);
+          resolve(message.result);
+        });
 
-// [database_api]
+        debugWs('Sending message', payload);
+        this.ws.send(payload);
+      })
+      .then(
+        (result) => callback(null, result),
+        (err) => callback(err)
+      ));
 
-var generatedMethods = require('./api-from-methods');
-generatedMethods(Steem);
+    return this.currentP;
+  }
+
+  streamBlockNumber(callback, ts = 200) {
+    let current = '';
+    let running = true;
 
-// [Stream]
+    const update = () => {
+      if (!running) return;
 
-Steem.streamBlockNumber = function(callback) {
-  var current = '';
-  var self = this;
-  setInterval(function() {
-    self.getDynamicGlobalProperties(function(err, result) {
-      var blockId = result.head_block_number;
-      if (blockId != current) {
-        current = blockId;
-        callback(null, current);
+      let result;
+      this.getDynamicGlobalPropertiesAsync()
+        .then((result) => {
+          const blockId = result.head_block_number;
+          if (blockId !== current) {
+            current = blockId;
+            callback(null, current);
+          }
+
+          Promise.delay(ts).then(() => {
+            update();
+          });
+        }, (err) => {
+          callback(err);
+        });
+    };
+
+    update();
+
+    return () => {
+      running = false;
+    };
+  }
+
+  streamBlock(callback) {
+    let current = '';
+    let last = '';
+
+    const release = this.streamBlockNumber((err, id) => {
+      if (err) {
+        release();
+        callback(err);
+        return;
+      }
+
+      current = id;
+      if (current !== last) {
+        last = current;
+        this.getBlock(current, callback);
       }
     });
-  }, 200);
-};
 
-Steem.streamBlock = function(callback) {
-  var current = '';
-  var last = '';
-  var self = this;
-  this.streamBlockNumber(function(err, id) {
-    current = id;
-    if (current != last) {
-      last = current;
-      self.getBlock(current, function(err, result) {
-        callback(null, result);
-      });
-    }
-  });
-};
+    return release;
+  }
+
+  streamTransactions(callback) {
+    const release = this.streamBlock((err, result) => {
+      if (err) {
+        release();
+        callback(err);
+        return;
+      }
 
-Steem.streamTransactions = function(callback) {
-  this.streamBlock(function(err, result) {
-    if (!!result) {
-      result.transactions.forEach(function(transaction) {
+      result.transactions.forEach((transaction) => {
         callback(null, transaction);
       });
-    }
-  })
-};
+    });
 
-Steem.streamOperations = function(callback) {
-  this.streamBlock(function(err, result) {
-    if (!!result) {
-      result.transactions.forEach(function(transaction) {
-        transaction.operations.forEach(function (operation) {
-          callback(null, operation);
-        });
+    return release;
+  }
+
+  streamOperations(callback) {
+    const release = this.streamTransactions((err, transaction) => {
+      if (err) {
+        release();
+        callback(err);
+        return;
+      }
+
+      transaction.operations.forEach(function (operation) {
+        callback(null, operation);
       });
-    }
-  })
-};
+    });
+
+    return release;
+  }
+}
+
+// Generate Methods from methods.json
+methods.reduce(function (memo, method) {
+  const methodName = camelCase(method.method);
+  const methodParams = method.params || [];
+
+  memo[methodName + 'With'] =
+    function Steem$$specializedSendWith(options, callback) {
+      const params = methodParams.map(function (param) {
+        return options[param];
+      });
+
+      return this.send(method.api, {
+        method: method.method,
+        params: params,
+      }, callback);
+    };
+
+  memo[methodName] =
+    function Steem$specializedSend(...args) {
+      const options = methodParams.reduce(function (memo, param, i) {
+        memo[param] = args[i];
+        return memo;
+      }, {});
+      const callback = args[methodParams.length];
+
+      return this[methodName + 'With'](options, callback);
+    };
+
+  return memo;
+}, Steem.prototype);
 
+Promise.promisifyAll(Steem.prototype);
 
-module.exports = Steem;
+// Export singleton instance
+const steem = new Steem();
+export default steem;
diff --git a/lib/broadcast.js b/lib/broadcast.js
index 1b02bda..0c14b62 100644
--- a/lib/broadcast.js
+++ b/lib/broadcast.js
@@ -1,569 +1,608 @@
-var steemAuth = require('steemauth'),
-	steemApi = require('./api');
-	formatter = require('./formatter');
+var steemAuth = require('steemauth');
+var steemApi = require('./api');
+var formatter = require('./formatter');
 
 module.exports = {
-	send: function(tx, privKeys, callback) {
-		steemApi.login('', '', function() {
-			steemApi.getDynamicGlobalProperties(function(err, result) {
-				var seconds = 1000;
-				result.timestamp = result.timestamp || Date.now()
-				var expiration = new Date(result.timestamp + 15 * seconds);
-				tx.expiration = expiration.toISOString().replace('Z', '');
-				tx.ref_block_num = result.head_block_number & 0xFFFF;
-				tx.ref_block_prefix =  new Buffer(result.head_block_id, 'hex').readUInt32LE(4);
-				var signedTransaction = steemAuth.signTransaction(tx, privKeys);
-				steemApi.broadcastTransactionWithCallback(function(){}, signedTransaction, function(err, result) {
-					callback(err, result);
-				});
-			});
-		});
-	},
-	vote: function(wif, voter, author, permlink, weight, callback) {
-		var tx = {
-			extensions: [],
-			operations: [['vote', {
-				voter: voter,
-				author: author,
-				permlink: permlink,
-				weight: weight
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result);
-		})
-	},
-	upvote: function(wif, voter, author, permlink, weight, callback) {
-		weight = weight || 10000;
-		vote(wif, author, permlink, weight, function(err, result) {
-			callback(err, result);
-		})
-	},
-	downvote: function(wif, voter, author, permlink, weight, callback) {
-		weight = weight || 10000;
-		vote(wif, author, permlink, -Math.abs(weight), function(err, result) {
-			callback(err, result);
-		})
-	},
-	comment: function(wif, parentAuthor, parentPermlink, author, permlink, title, body, jsonMetadata, callback) {
-		permlink = permlink || formatter.commentPermlink(parentAuthor, parentPermlink);
-		var tx = {
-			extensions: [],
-			operations: [['comment', {
-				parent_author: parentAuthor,
-				parent_permlink: parentPermlink,
-				author: author,
-				permlink: permlink,
-				title: title,
-				body: body,
-				json_metadata: JSON.stringify(jsonMetadata)
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result);
-		})
-	},
-	transfer: function(wif, from, to, amount, memo, callback) {
-		var tx = {
-			extensions: [],
-			operations: [['transfer', {
-				from: from,
-				to: to,
-				amount: amount,
-				memo: memo
-			}]]
-		};
-		this.send(tx, {active: wif}, function(err, result) {
-			callback(err, result);
-		})
-	},
-	transferToVesting: function(wif, from, to, amount, callback) {
-		var tx = {
-			extensions: [],
-			operations: [['transfer_to_vesting', {
-				from: from,
-				to: to,
-				amount: amount
-			}]]
-		};
-		this.send(tx, {active: wif}, function(err, result) {
-			callback(err, result);
-		})
-	},
-	withdrawVesting: function(wif, account, vestingShares, callback) {
-		var tx = {
-			extensions: [],
-			operations: [['withdraw_vesting', {
-				account: account,
-    			vesting_shares: vestingShares
-			}]]
-		};
-		this.send(tx, {active: wif}, function(err, result) {
-			callback(err, result);
-		})
-	},
-	limitOrderCreate: function(wif, owner, orderid, amountToSell, minToReceive, fillOrKill, expiration, callback) {
-		var tx = {
-			extensions: [],
-			operations:[['limit_order_create', {
-				owner: owner,
-				orderid: orderid,
-				amount_to_sell: amountToSell,
-				min_to_receive: minToReceive,
-				fill_or_kill: fillOrKill,
-				expiration: expiration
-			}]]
-		};
-		this.send(tx, {active: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	limitOrderCancel: function(wif, owner, orderid, callback) {
-		var tx = {
-			extensions: [],
-			operations:[['limit_order_cancel', {
-				owner: owner,
-				orderid: orderid
-			}]]
-		};
-		this.send(tx, {active: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	feedPublish: function(wif, publisher, exchangeRate, callback) {
-		var tx = {
-			extensions: [],
-			operations:[['feed_publish', {
-				publisher: publisher,
-				exchange_rate: exchangeRate
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	convert: function(wif, owner, requestid, amount, callback) {
-		var tx = {
-			extensions: [],
-			operations:[['convert', {
-				owner: owner,
-				requestid: requestid,
-				amount: amount
-			}]]
-		};
-		this.send(tx, {active: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	accountCreate: function(wif, fee, creator, newAccountName, owner, active, posting, memoKey, jsonMetadata, callback) {
-		var tx = {
-			extensions: [],
-			operations:[['account_create', {
-				fee: fee,
-				creator: creator,
-				new_account_name: newAccountName,
-				owner: owner,
-				active: active,
-				posting: posting,
-				memo_key: memoKey,
-				json_metadata: JSON.stringify(jsonMetadata)
-			}]]
-		};
-		this.send(tx, {owner: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	accountUpdate: function(wif, account, owner, active, posting, memoKey, jsonMetadata, callback) {
-		var tx = {
-			extensions: [],
-			operations:[['account_update', {
-				account: account,
-				owner: owner,
-				active: active,
-				posting: posting,
-				memo_key: memoKey,
-				json_metadata: JSON.stringify(jsonMetadata)
-			}]]
-		};
-		this.send(tx, {owner: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	witnessUpdate: function(wif, owner, url, blockSigningKey, props, fee, callback) {
-		var tx = {
-			extensions: [],
-			operations:[['witness_update', {
-				owner: owner,
-				url: url,
-				block_signing_key: blockSigningKey,
-				props: props,
-				fee: fee
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	accountWitnessVote: function(wif, account, witness, approve, callback) {
-		var tx = {
-			extensions: [],
-			operations:[['account_witness_vote', {
-				account: account,
-				witness: witness,
-				approve: approve
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	accountWitnessProxy: function(wif, account, proxy, callback) {
-		var tx = {
-			extensions: [],
-			operations:[['account_witness_proxy', {
-				account: account,
-				proxy: proxy
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	pow: function(wif, worker, input, signature, work, callback) {
-		var tx = {
-			extensions: [],
-			operations:[['pow', {
-				worker: worker,
-				input: input,
-				signature: signature,
-				work: work
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	custom: function(wif, requiredAuths, id, data, callback) {
-		var tx = {
-			extensions: [],
-			operations:[['custom', {
-				required_auths: requiredAuths,
-				id: id,
-				data: data
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	reportOverProduction: function(wif, reporter, firstBlock, secondBlock, callback) {
-		var tx = {
-			extensions: [],
-			operations:[['report_over_production', {
-				reporter: reporter,
-				first_block: firstBlock,
-				second_block: secondBlock
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	deleteComment: function(wif, author, permlink, callback) {
-		var tx = {
-			extensions: [],
-			operations:[['delete_comment', {
-				author: author,
-				permlink: permlink
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	customJson: function(wif, requiredAuths, requiredPostingAuths, id, json, callback) {
-		var tx = {
-			extensions: [],
-			operations:[['custom_json', {
-				required_auths: requiredAuths,
-				required_posting_auths: requiredPostingAuths,
-				id: id,
-				json: json
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	commentOptions: function(wif, author, permlink, maxAcceptedPayout, percentSteemDollars, allowVotes, allowCurationRewards, extensions, callback) {
-		var tx = {
-			extensions: [],
-			operations:[['comment_options', {
-				author: author,
-				permlink: permlink,
-				max_accepted_payout: maxAcceptedPayout,
-				percent_steem_dollars: percentSteemDollars,
-				allow_votes: allowVotes,
-				allow_curation_rewards: allowCurationRewards,
-				extensions: extensions
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	setWithdrawVestingRoute: function(wif, fromAccount, toAccount, percent, autoVest, callback) {
-		var tx = {
-			extensions: [],
-			operations:[['set_withdraw_vesting_route', {
-				from_account: fromAccount,
-				to_account: toAccount,
-				percent: percent,
-				auto_vest: autoVest
-			}]]
-		};
-		this.send(tx, {active: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	limitOrderCreate2: function(wif, owner, orderid, amountToSell, exchangeRate, fillOrKill, expiration, callback) {
-		var tx = {
-			extensions: [],
-			operations:[['limit_order_create2', {
-				owner: owner,
-				orderid: orderid,
-				amount_to_sell: amountToSell,
-				exchange_rate: exchangeRate,
-				fill_or_kill: fillOrKill,
-				expiration: expiration
-			}]]
-		};
-		this.send(tx, {active: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	challengeAuthority: function(wif, challenger, challenged, requireOwner, callback){
-		var tx = {
-			extensions: [],
-			operations: [['challenge_authority', {
-				challenger: challenger,
-				challenged: challenged,
-				require_owner: requireOwner
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	proveAuthority: function(wif, challenged, requireOwner, callback){
-		var tx = {
-			extensions: [],
-			operations: [['prove_authority', {
-				challenged: challenged,
-				require_owner: requireOwner
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	requestAccountRecovery: function(wif, recoveryAccount, accountToRecover, newOwnerAuthority, extensions, callback){
-		var tx = {
-			extensions: [],
-			operations: [['request_account_recovery', {
-				recovery_account: recoveryAccount,
-				account_to_recover: accountToRecover,
-				new_owner_authority: newOwnerAuthority,
-				extensions: extensions
-			}]]
-		};
-		this.send(tx, {owner: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	recoverAccount: function(wif, accountToRecover, newOwnerAuthority, recentOwnerAuthority, extensions, callback){
-		var tx = {
-			extensions: [],
-			operations: [['recover_account', {
-				account_to_recover: accountToRecover,
-				new_owner_authority: newOwnerAuthority,
-				recent_owner_authority: recentOwnerAuthority,
-				extensions: extensions
-			}]]
-		};
-		this.send(tx, {owner: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	changeRecoveryAccount: function(wif, accountToRecover, newRecoveryAccount, extensions, callback){
-		var tx = {
-			extensions: [],
-			operations: [['change_recovery_account', {
-				account_to_recover: accountToRecover,
-				new_recovery_account: newRecoveryAccount,
-				extensions: extensions
-			}]]
-		};
-		this.send(tx, {owner: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	escrowTransfer: function(wif, from, to, amount, memo, escrowId, agent, fee, jsonMeta, expiration, callback){
-		var tx = {
-			extensions: [],
-			operations: [['escrow_transfer', {
-				from: from,
-				to: to,
-				amount: amount,
-				memo: memo,
-				escrow_id: escrowId,
-				agent: agent,
-				fee: fee,
-				json_meta: jsonMeta,
-				expiration: expiration
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	escrowDispute: function(wif, from, to, escrowId, who, callback){
-		var tx = {
-			extensions: [],
-			operations: [['escrow_dispute', {
-				from: from,
-				to: to,
-				escrow_id: escrowId,
-				who: who
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	escrowRelease: function(wif, from, to, escrowId, who, amount, callback){
-		var tx = {
-			extensions: [],
-			operations: [['escrow_release', {
-				from: from,
-				to: to,
-				escrow_id: escrowId,
-				who: who,
-				amount: amount
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	fillConvertRequest: function(wif, owner, requestid, amountIn, amountOut, callback){
-		var tx = {
-			extensions: [],
-			operations: [['fill_convert_request', {
-				owner: owner,
-				requestid: requestid,
-				amount_in: amountIn,
-				amount_out: amountOut
-			}]]
-		};
-		this.send(tx, {active: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	commentReward: function(wif, author, permlink, sbdPayout, vestingPayout, callback){
-		var tx = {
-			extensions: [],
-			operations: [['comment_reward', {
-				author: author,
-				permlink: permlink,
-				sbd_payout: sbdPayout,
-				vesting_payout: vestingPayout
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	curateReward: function(wif, curator, reward, commentAuthor, commentPermlink, callback){
-		var tx = {
-			extensions: [],
-			operations: [['curate_reward', {
-				curator: curator,
-				reward: reward,
-				comment_author: commentAuthor,
-				comment_permlink: commentPermlink
-			}]]
-		};
-		this.send(tx, {active: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	liquidityReward: function(wif, owner, payout, callback){
-		var tx = {
-			extensions: [],
-			operations: [['liquidity_reward', {
-				owner: owner,
-				payout: payout
-			}]]
-		};
-		this.send(tx, {active: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	interest: function(wif, owner, interest, callback){
-		var tx = {
-			extensions: [],
-			operations: [['interest', {
-				owner: owner,
-				interest: interest
-			}]]
-		};
-		this.send(tx, {active: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	fillVestingWithdraw: function(wif, fromAccount, toAccount, withdrawn, deposited, callback){
-		var tx = {
-			extensions: [],
-			operations: [['fill_vesting_withdraw', {
-				from_account: fromAccount,
-				to_account: toAccount,
-				withdrawn: withdrawn,
-				deposited: deposited
-			}]]
-		};
-		this.send(tx, {active: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	fillOrder: function(wif, currentOwner, currentOrderid, currentPays, openOwner, openOrderid, openPays, callback){
-		var tx = {
-			extensions: [],
-			operations: [['fill_order', {
-				current_owner: currentOwner,
-				current_orderid: currentOrderid,
-				current_pays: currentPays,
-				open_owner: openOwner,
-				open_orderid: openOrderid,
-				open_pays: openPays
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	},
-	commentPayout: function(wif, author, permlink, payout, callback){
-		var tx = {
-			extensions: [],
-			operations: [['comment_payout', {
-				author: author,
-				permlink: permlink,
-				payout: payout
-			}]]
-		};
-		this.send(tx, {posting: wif}, function(err, result) {
-			callback(err, result)
-		})
-	}
+  send: function(tx, privKeys, callback) {
+    steemApi.login('', '', function() {
+      steemApi.getDynamicGlobalProperties(function(err, result) {
+        var seconds = 1000;
+        result.timestamp = result.timestamp || Date.now()
+          var expiration = new Date(result.timestamp + 15 * seconds);
+        tx.expiration = expiration.toISOString().replace('Z', '');
+        tx.ref_block_num = result.head_block_number & 0xFFFF;
+        tx.ref_block_prefix =  new Buffer(result.head_block_id, 'hex').readUInt32LE(4);
+        var signedTransaction = steemAuth.signTransaction(tx, privKeys);
+        steemApi.broadcastTransactionWithCallback(function(){}, signedTransaction, function(err, result) {
+          callback(err, result);
+        });
+      });
+    });
+  },
 
+  vote: function(wif, voter, author, permlink, weight, callback) {
+    var tx = {
+      extensions: [],
+      operations: [['vote', {
+        voter: voter,
+        author: author,
+        permlink: permlink,
+        weight: weight
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result);
+    })
+  },
+
+  upvote: function(wif, voter, author, permlink, weight, callback) {
+    weight = weight || 10000;
+    vote(wif, author, permlink, weight, function(err, result) {
+      callback(err, result);
+    })
+  },
+
+  downvote: function(wif, voter, author, permlink, weight, callback) {
+    weight = weight || 10000;
+    vote(wif, author, permlink, -Math.abs(weight), function(err, result) {
+      callback(err, result);
+    })
+  },
+
+  comment: function(wif, parentAuthor, parentPermlink, author, permlink, title, body, jsonMetadata, callback) {
+    permlink = permlink || formatter.commentPermlink(parentAuthor, parentPermlink);
+    var tx = {
+      extensions: [],
+      operations: [['comment', {
+        parent_author: parentAuthor,
+        parent_permlink: parentPermlink,
+        author: author,
+        permlink: permlink,
+        title: title,
+        body: body,
+        json_metadata: JSON.stringify(jsonMetadata)
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result);
+    })
+  },
+
+  transfer: function(wif, from, to, amount, memo, callback) {
+    var tx = {
+      extensions: [],
+      operations: [['transfer', {
+        from: from,
+        to: to,
+        amount: amount,
+        memo: memo
+      }]]
+    };
+    this.send(tx, {active: wif}, function(err, result) {
+      callback(err, result);
+    })
+  },
+
+  transferToVesting: function(wif, from, to, amount, callback) {
+    var tx = {
+      extensions: [],
+      operations: [['transfer_to_vesting', {
+        from: from,
+        to: to,
+        amount: amount
+      }]]
+    };
+    this.send(tx, {active: wif}, function(err, result) {
+      callback(err, result);
+    })
+  },
+
+  withdrawVesting: function(wif, account, vestingShares, callback) {
+    var tx = {
+      extensions: [],
+      operations: [['withdraw_vesting', {
+        account: account,
+        vesting_shares: vestingShares
+      }]]
+    };
+    this.send(tx, {active: wif}, function(err, result) {
+      callback(err, result);
+    })
+  },
+
+  limitOrderCreate: function(wif, owner, orderid, amountToSell, minToReceive, fillOrKill, expiration, callback) {
+    var tx = {
+      extensions: [],
+      operations:[['limit_order_create', {
+        owner: owner,
+        orderid: orderid,
+        amount_to_sell: amountToSell,
+        min_to_receive: minToReceive,
+        fill_or_kill: fillOrKill,
+        expiration: expiration
+      }]]
+    };
+    this.send(tx, {active: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  limitOrderCancel: function(wif, owner, orderid, callback) {
+    var tx = {
+      extensions: [],
+      operations:[['limit_order_cancel', {
+        owner: owner,
+        orderid: orderid
+      }]]
+    };
+    this.send(tx, {active: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  feedPublish: function(wif, publisher, exchangeRate, callback) {
+    var tx = {
+      extensions: [],
+      operations:[['feed_publish', {
+        publisher: publisher,
+        exchange_rate: exchangeRate
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  convert: function(wif, owner, requestid, amount, callback) {
+    var tx = {
+      extensions: [],
+      operations:[['convert', {
+        owner: owner,
+        requestid: requestid,
+        amount: amount
+      }]]
+    };
+    this.send(tx, {active: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  accountCreate: function(wif, fee, creator, newAccountName, owner, active, posting, memoKey, jsonMetadata, callback) {
+    var tx = {
+      extensions: [],
+      operations:[['account_create', {
+        fee: fee,
+        creator: creator,
+        new_account_name: newAccountName,
+        owner: owner,
+        active: active,
+        posting: posting,
+        memo_key: memoKey,
+        json_metadata: JSON.stringify(jsonMetadata)
+      }]]
+    };
+    this.send(tx, {owner: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  accountUpdate: function(wif, account, owner, active, posting, memoKey, jsonMetadata, callback) {
+    var tx = {
+      extensions: [],
+      operations:[['account_update', {
+        account: account,
+        owner: owner,
+        active: active,
+        posting: posting,
+        memo_key: memoKey,
+        json_metadata: JSON.stringify(jsonMetadata)
+      }]]
+    };
+    this.send(tx, {owner: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  witnessUpdate: function(wif, owner, url, blockSigningKey, props, fee, callback) {
+    var tx = {
+      extensions: [],
+      operations:[['witness_update', {
+        owner: owner,
+        url: url,
+        block_signing_key: blockSigningKey,
+        props: props,
+        fee: fee
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  accountWitnessVote: function(wif, account, witness, approve, callback) {
+    var tx = {
+      extensions: [],
+      operations:[['account_witness_vote', {
+        account: account,
+        witness: witness,
+        approve: approve
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  accountWitnessProxy: function(wif, account, proxy, callback) {
+    var tx = {
+      extensions: [],
+      operations:[['account_witness_proxy', {
+        account: account,
+        proxy: proxy
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  pow: function(wif, worker, input, signature, work, callback) {
+    var tx = {
+      extensions: [],
+      operations:[['pow', {
+        worker: worker,
+        input: input,
+        signature: signature,
+        work: work
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  custom: function(wif, requiredAuths, id, data, callback) {
+    var tx = {
+      extensions: [],
+      operations:[['custom', {
+        required_auths: requiredAuths,
+        id: id,
+        data: data
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  reportOverProduction: function(wif, reporter, firstBlock, secondBlock, callback) {
+    var tx = {
+      extensions: [],
+      operations:[['report_over_production', {
+        reporter: reporter,
+        first_block: firstBlock,
+        second_block: secondBlock
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  deleteComment: function(wif, author, permlink, callback) {
+    var tx = {
+      extensions: [],
+      operations:[['delete_comment', {
+        author: author,
+        permlink: permlink
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  customJson: function(wif, requiredAuths, requiredPostingAuths, id, json, callback) {
+    var tx = {
+      extensions: [],
+      operations:[['custom_json', {
+        required_auths: requiredAuths,
+        required_posting_auths: requiredPostingAuths,
+        id: id,
+        json: json
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  commentOptions: function(wif, author, permlink, maxAcceptedPayout, percentSteemDollars, allowVotes, allowCurationRewards, extensions, callback) {
+    var tx = {
+      extensions: [],
+      operations:[['comment_options', {
+        author: author,
+        permlink: permlink,
+        max_accepted_payout: maxAcceptedPayout,
+        percent_steem_dollars: percentSteemDollars,
+        allow_votes: allowVotes,
+        allow_curation_rewards: allowCurationRewards,
+        extensions: extensions
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  setWithdrawVestingRoute: function(wif, fromAccount, toAccount, percent, autoVest, callback) {
+    var tx = {
+      extensions: [],
+      operations:[['set_withdraw_vesting_route', {
+        from_account: fromAccount,
+        to_account: toAccount,
+        percent: percent,
+        auto_vest: autoVest
+      }]]
+    };
+    this.send(tx, {active: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  limitOrderCreate2: function(wif, owner, orderid, amountToSell, exchangeRate, fillOrKill, expiration, callback) {
+    var tx = {
+      extensions: [],
+      operations:[['limit_order_create2', {
+        owner: owner,
+        orderid: orderid,
+        amount_to_sell: amountToSell,
+        exchange_rate: exchangeRate,
+        fill_or_kill: fillOrKill,
+        expiration: expiration
+      }]]
+    };
+    this.send(tx, {active: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  challengeAuthority: function(wif, challenger, challenged, requireOwner, callback){
+    var tx = {
+      extensions: [],
+      operations: [['challenge_authority', {
+        challenger: challenger,
+        challenged: challenged,
+        require_owner: requireOwner
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  proveAuthority: function(wif, challenged, requireOwner, callback){
+    var tx = {
+      extensions: [],
+      operations: [['prove_authority', {
+        challenged: challenged,
+        require_owner: requireOwner
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  requestAccountRecovery: function(wif, recoveryAccount, accountToRecover, newOwnerAuthority, extensions, callback){
+    var tx = {
+      extensions: [],
+      operations: [['request_account_recovery', {
+        recovery_account: recoveryAccount,
+        account_to_recover: accountToRecover,
+        new_owner_authority: newOwnerAuthority,
+        extensions: extensions
+      }]]
+    };
+    this.send(tx, {owner: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  recoverAccount: function(wif, accountToRecover, newOwnerAuthority, recentOwnerAuthority, extensions, callback){
+    var tx = {
+      extensions: [],
+      operations: [['recover_account', {
+        account_to_recover: accountToRecover,
+        new_owner_authority: newOwnerAuthority,
+        recent_owner_authority: recentOwnerAuthority,
+        extensions: extensions
+      }]]
+    };
+    this.send(tx, {owner: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  changeRecoveryAccount: function(wif, accountToRecover, newRecoveryAccount, extensions, callback){
+    var tx = {
+      extensions: [],
+      operations: [['change_recovery_account', {
+        account_to_recover: accountToRecover,
+        new_recovery_account: newRecoveryAccount,
+        extensions: extensions
+      }]]
+    };
+    this.send(tx, {owner: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  escrowTransfer: function(wif, from, to, amount, memo, escrowId, agent, fee, jsonMeta, expiration, callback){
+    var tx = {
+      extensions: [],
+      operations: [['escrow_transfer', {
+        from: from,
+        to: to,
+        amount: amount,
+        memo: memo,
+        escrow_id: escrowId,
+        agent: agent,
+        fee: fee,
+        json_meta: jsonMeta,
+        expiration: expiration
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  escrowDispute: function(wif, from, to, escrowId, who, callback){
+    var tx = {
+      extensions: [],
+      operations: [['escrow_dispute', {
+        from: from,
+        to: to,
+        escrow_id: escrowId,
+        who: who
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  escrowRelease: function(wif, from, to, escrowId, who, amount, callback){
+    var tx = {
+      extensions: [],
+      operations: [['escrow_release', {
+        from: from,
+        to: to,
+        escrow_id: escrowId,
+        who: who,
+        amount: amount
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  fillConvertRequest: function(wif, owner, requestid, amountIn, amountOut, callback){
+    var tx = {
+      extensions: [],
+      operations: [['fill_convert_request', {
+        owner: owner,
+        requestid: requestid,
+        amount_in: amountIn,
+        amount_out: amountOut
+      }]]
+    };
+    this.send(tx, {active: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  commentReward: function(wif, author, permlink, sbdPayout, vestingPayout, callback){
+    var tx = {
+      extensions: [],
+      operations: [['comment_reward', {
+        author: author,
+        permlink: permlink,
+        sbd_payout: sbdPayout,
+        vesting_payout: vestingPayout
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  curateReward: function(wif, curator, reward, commentAuthor, commentPermlink, callback){
+    var tx = {
+      extensions: [],
+      operations: [['curate_reward', {
+        curator: curator,
+        reward: reward,
+        comment_author: commentAuthor,
+        comment_permlink: commentPermlink
+      }]]
+    };
+    this.send(tx, {active: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  liquidityReward: function(wif, owner, payout, callback){
+    var tx = {
+      extensions: [],
+      operations: [['liquidity_reward', {
+        owner: owner,
+        payout: payout
+      }]]
+    };
+    this.send(tx, {active: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  interest: function(wif, owner, interest, callback){
+    var tx = {
+      extensions: [],
+      operations: [['interest', {
+        owner: owner,
+        interest: interest
+      }]]
+    };
+    this.send(tx, {active: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  fillVestingWithdraw: function(wif, fromAccount, toAccount, withdrawn, deposited, callback){
+    var tx = {
+      extensions: [],
+      operations: [['fill_vesting_withdraw', {
+        from_account: fromAccount,
+        to_account: toAccount,
+        withdrawn: withdrawn,
+        deposited: deposited
+      }]]
+    };
+    this.send(tx, {active: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  fillOrder: function(wif, currentOwner, currentOrderid, currentPays, openOwner, openOrderid, openPays, callback){
+    var tx = {
+      extensions: [],
+      operations: [['fill_order', {
+        current_owner: currentOwner,
+        current_orderid: currentOrderid,
+        current_pays: currentPays,
+        open_owner: openOwner,
+        open_orderid: openOrderid,
+        open_pays: openPays
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  },
+
+  commentPayout: function(wif, author, permlink, payout, callback){
+    var tx = {
+      extensions: [],
+      operations: [['comment_payout', {
+        author: author,
+        permlink: permlink,
+        payout: payout
+      }]]
+    };
+    this.send(tx, {posting: wif}, function(err, result) {
+      callback(err, result)
+    })
+  }
 };
diff --git a/lib/browser.js b/lib/browser.js
index ad353f7..8d6aaff 100644
--- a/lib/browser.js
+++ b/lib/browser.js
@@ -3,4 +3,4 @@ steem = {
 	formatter: require('./formatter'),
 	connect: require('steemconnect'),
 	embed: require('steemembed')
-};
\ No newline at end of file
+};
diff --git a/lib/formatter.js b/lib/formatter.js
index 6738cb9..9f88e52 100644
--- a/lib/formatter.js
+++ b/lib/formatter.js
@@ -1,31 +1,34 @@
 module.exports = {
-	reputation: function (reputation) {
-		if (reputation == null) return reputation;
-		reputation = parseInt(reputation);
-		var rep = String(reputation);
-		var neg = rep.charAt(0) === '-';
-		rep = neg ? rep.substring(1) : rep;
-		var str = rep;
-		var leadingDigits = parseInt(str.substring(0, 4));
-		var log = Math.log(leadingDigits) / Math.log(10);
-		var n = str.length - 1;
-		var out = n + (log - parseInt(log));
-		if (isNaN(out)) out = 0;
-		out = Math.max(out - 9, 0);
-		out = (neg ? -1 : 1) * out;
-		out = (out * 9) + 25;
-		out = parseInt(out);
-		return out;
-	},
-	vestToSteem: function(vestingShares, totalVestingShares, totalVestingFundSteem) {
-		return parseFloat(totalVestingFundSteem) * (parseFloat(vestingShares) / parseFloat(totalVestingShares));
-	},
-	commentPermlink: function(parentAuthor, parentPermlink) {
-		var timeStr = new Date().toISOString().replace(/[^a-zA-Z0-9]+/g, '');
-		parentPermlink = parentPermlink.replace(/(-\d{8}t\d{9}z)/g, '');
-		return 're-' + parentAuthor + '-' + parentPermlink + '-' + timeStr;
-	},
-	amount: function(amount, asset) {
-		return amount.toFixed(3) + ' ' + asset;
-	}
-};
\ No newline at end of file
+  reputation: function (reputation) {
+    if (reputation == null) return reputation;
+    reputation = parseInt(reputation);
+    var rep = String(reputation);
+    var neg = rep.charAt(0) === '-';
+    rep = neg ? rep.substring(1) : rep;
+    var str = rep;
+    var leadingDigits = parseInt(str.substring(0, 4));
+    var log = Math.log(leadingDigits) / Math.log(10);
+    var n = str.length - 1;
+    var out = n + (log - parseInt(log));
+    if (isNaN(out)) out = 0;
+    out = Math.max(out - 9, 0);
+    out = (neg ? -1 : 1) * out;
+    out = (out * 9) + 25;
+    out = parseInt(out);
+    return out;
+  },
+
+  vestToSteem: function(vestingShares, totalVestingShares, totalVestingFundSteem) {
+    return parseFloat(totalVestingFundSteem) * (parseFloat(vestingShares) / parseFloat(totalVestingShares));
+  },
+
+  commentPermlink: function(parentAuthor, parentPermlink) {
+    var timeStr = new Date().toISOString().replace(/[^a-zA-Z0-9]+/g, '');
+    parentPermlink = parentPermlink.replace(/(-\d{8}t\d{9}z)/g, '');
+    return 're-' + parentAuthor + '-' + parentPermlink + '-' + timeStr;
+  },
+
+  amount: function(amount, asset) {
+    return amount.toFixed(3) + ' ' + asset;
+  }
+};
diff --git a/lib/util.js b/lib/util.js
new file mode 100644
index 0000000..ae9f87d
--- /dev/null
+++ b/lib/util.js
@@ -0,0 +1,6 @@
+const snakeCaseRe = /_([a-z])/g;
+export function camelCase(str) {
+  return str.replace(snakeCaseRe, function (_m, l) {
+    return l.toUpperCase();
+  });
+}
diff --git a/package.json b/package.json
index a33328c..fb41034 100644
--- a/package.json
+++ b/package.json
@@ -4,8 +4,8 @@
   "description": "Steem.js the JavaScript API for Steem blockchain",
   "main": "index.js",
   "scripts": {
-    "test": "mocha --require babel-register",
-    "build": "browserify lib/browser.js -o examples/steem.js && uglifyjs examples/steem.js -o examples/steem.min.js && uglifyjs examples/steem.js -o steem.min.js"
+    "test": "mocha -t 10000 --require babel-polyfill --require babel-register",
+    "build": "rm -rf dist && NODE_ENV=production webpack && gzip -k -f ./dist/*.js && du -h ./dist/*"
   },
   "browser": {
     "ws": false
@@ -27,6 +27,8 @@
   },
   "homepage": "https://github.com/adcpm/steem#readme",
   "dependencies": {
+    "bluebird": "^3.4.6",
+    "debug": "^2.2.0",
     "detect-node": "^2.0.3",
     "steemauth": "0.0.16",
     "steemconnect": "0.0.25",
@@ -34,16 +36,20 @@
     "ws": "^1.1.1"
   },
   "devDependencies": {
-    "babel-polyfill": "^6.13.0",
-    "babel-preset-es2017": "^6.14.0",
+    "babel-loader": "^6.2.5",
+    "babel-polyfill": "^6.16.0",
+    "babel-preset-es2015": "^6.16.0",
+    "babel-preset-es2017": "^6.16.0",
     "babel-register": "^6.14.0",
     "bluebird": "^3.4.6",
     "browserify": "^13.0.1",
     "bufferutil": "^1.2.1",
+    "json-loader": "^0.5.4",
     "mocha": "^3.0.2",
     "should": "^11.1.0",
     "uglifyjs": "^2.4.10",
-    "utf-8-validate": "^1.2.1"
+    "utf-8-validate": "^1.2.1",
+    "webpack-visualizer-plugin": "^0.1.5"
   },
   "contributors": [
     "Fabien (https://github.com/adcpm)",
diff --git a/test/api.test.js b/test/api.test.js
index f8ea5eb..796cf6d 100644
--- a/test/api.test.js
+++ b/test/api.test.js
@@ -1,24 +1,135 @@
-const Promise = require('bluebird');
-const should = require('should');
+import Promise from 'bluebird';
+import should from 'should';
 
-const Steem = require('..');
-Promise.promisifyAll(Steem.api);
+import steem from '../lib/api';
+import testPost from './test-post.json';
+
+describe('steem', function () {
+  this.timeout(10000);
+
+  before(async () => {
+    await steem.apiIdsP;
+  });
+
+  describe('getFollowers', () => {
+    describe('getting ned\'s followers', () => {
+      it('works', async () => {
+        const result = await steem.getFollowersAsync('ned', 0, 'blog', 5);
+        result.should.have.lengthOf(5);
+      });
+
+      it('clears listeners', async () => {
+        steem.listeners('message').should.have.lengthOf(0);
+      });
+    });
+  });
+
+  describe('getContent', () => {
+    describe('getting a random post', () => {
+      it('works', async () => {
+        const result = await steem.getContentAsync('yamadapc', 'test-1-2-3-4-5-6-7-9');
+        result.should.have.properties(testPost);
+      });
+
+      it('clears listeners', async () => {
+        steem.listeners('message').should.have.lengthOf(0);
+      });
+    });
+  });
 
-describe('steem', () => {
   describe('getFollowers', () => {
     describe('getting ned\'s followers', () => {
       it('works', async () => {
-        const result = await Steem.api.getFollowersAsync('ned', 0, 'blog', 5)
-          result.should.have.lengthOf(5);
+        const result = await steem.getFollowersAsync('ned', 0, 'blog', 5);
+        result.should.have.lengthOf(5);
+      });
+    });
+  });
+
+  describe('streamBlockNumber', () => {
+    it('streams steem transactions', (done) => {
+      let i = 0;
+      const release = steem.streamBlockNumber((err, block) => {
+        should.exist(block);
+        block.should.be.instanceOf(Number);
+        i++;
+        if (i === 2) {
+          release();
+          done();
+        }
+      });
+    });
+  });
+
+  describe('streamBlock', () => {
+    it('streams steem blocks', (done) => {
+      let i = 0;
+      const release = steem.streamBlock((err, block) => {
+        try {
+          should.exist(block);
+          block.should.have.properties([
+            'previous',
+            'transactions',
+            'timestamp',
+          ]);
+        } catch (err) {
+          release();
+          done(err);
+          return;
+        }
+
+        i++;
+        if (i === 2) {
+          release();
+          done();
+        }
       });
+    });
+  });
+
+  describe('streamTransactions', () => {
+    it('streams steem transactions', (done) => {
+      let i = 0;
+      const release = steem.streamTransactions((err, transaction) => {
+        try {
+          should.exist(transaction);
+          transaction.should.have.properties([
+            'ref_block_num',
+            'operations',
+            'extensions',
+          ]);
+        } catch (err) {
+          release();
+          done(err);
+          return;
+        }
+
+        i++;
+        if (i === 2) {
+          release();
+          done();
+        }
+      });
+    });
+  });
+
+  describe('streamOperations', () => {
+    it('streams steem operations', (done) => {
+      let i = 0;
+      const release = steem.streamOperations((err, operation) => {
+        try {
+          should.exist(operation);
+        } catch (err) {
+          release();
+          done(err);
+          return;
+        }
 
-      it.skip('the startFollower parameter has an impact on the result', async () => {
-        // Get the first 5
-        const result1 = await Steem.api.getFollowersAsync('ned', 0, 'blog', 5)
-          result1.should.have.lengthOf(5);
-        const result2 = await Steem.api.getFollowersAsync('ned', 5, 'blog', 5)
-          result2.should.have.lengthOf(5);
-        result1.should.not.be.eql(result2);
+        i++;
+        if (i === 2) {
+          release();
+          done();
+        }
       });
     });
   });
diff --git a/test/test-post.json b/test/test-post.json
new file mode 100644
index 0000000..3968069
--- /dev/null
+++ b/test/test-post.json
@@ -0,0 +1,13 @@
+{
+  "author": "yamadapc",
+  "permlink": "test-1-2-3-4-5-6-7-9",
+  "category": "test",
+  "parent_author": "",
+  "parent_permlink": "test",
+  "title": "test-1-2-3-4-5-6-7-9",
+  "body": "<script>alert('hello world')</script>",
+  "allow_replies": true,
+  "allow_votes": true,
+  "allow_curation_rewards": true,
+  "url": "/test/@yamadapc/test-1-2-3-4-5-6-7-9"
+}
diff --git a/test/test.html b/test/test.html
new file mode 100644
index 0000000..f771c7f
--- /dev/null
+++ b/test/test.html
@@ -0,0 +1,13 @@
+<link href="../node_modules/mocha/mocha.css" rel="stylesheet" />
+
+<div id="mocha"></div>
+
+<script src="../node_modules/mocha/mocha.js"></script>
+<script>mocha.setup('bdd')</script>
+<script src="../dist/steemjs-tests.min.js"></script>
+
+
+<script>
+  mocha.checkLeaks();
+  mocha.run();
+</script>
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..e3b3ddb
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,2 @@
+const makeConfig = require('./webpack/makeConfig');
+exports = module.exports = makeConfig();
diff --git a/webpack/makeConfig.js b/webpack/makeConfig.js
new file mode 100644
index 0000000..8ea821f
--- /dev/null
+++ b/webpack/makeConfig.js
@@ -0,0 +1,105 @@
+'use strict';
+const Visualizer = require('webpack-visualizer-plugin');
+const _ = require('lodash');
+const path = require('path');
+const webpack = require('webpack');
+
+const DEFAULTS = {
+  isDevelopment: process.env.NODE_ENV !== 'production',
+  baseDir: path.join(__dirname, '..'),
+};
+
+function makePlugins(options) {
+  const isDevelopment = options.isDevelopment;
+
+  let plugins = [
+    new Visualizer({
+      filename: './statistics.html'
+    }),
+  ];
+
+  if (!isDevelopment) {
+    plugins = plugins.concat([
+      new webpack.optimize.DedupePlugin(),
+      new webpack.optimize.UglifyJsPlugin({
+        minimize: true,
+        compress: {
+          warnings: false,
+        }
+      }),
+      new webpack.optimize.AggressiveMergingPlugin(),
+    ]);
+  }
+
+  return plugins;
+}
+
+function makeStyleLoaders(options) {
+  if (options.isDevelopment) {
+    return [
+      {
+        test: /\.s[ac]ss$/,
+        loaders: [
+          'style',
+          'css?sourceMap',
+          'autoprefixer-loader?browsers=last 2 version',
+          'sass?sourceMap&sourceMapContents',
+        ],
+      },
+    ];
+  }
+
+  return [
+    {
+      test: /\.s[ac]ss$/,
+      loader: ExtractTextPlugin.extract(
+        'style-loader',
+        'css!autoprefixer-loader?browsers=last 2 version!sass'
+      ),
+    },
+  ];
+}
+
+function makeConfig(options) {
+  if (!options) options = {};
+  _.defaults(options, DEFAULTS);
+
+  const isDevelopment = options.isDevelopment;
+
+  return {
+    devtool: isDevelopment ? 'cheap-eval-source-map' : 'source-map',
+    entry: isDevelopment ? {
+      steemjs: path.join(options.baseDir, 'lib/browser.js'),
+      'steemjs-tests': path.join(options.baseDir, 'test/api.test.js'),
+    } : {
+      steemjs: path.join(options.baseDir, 'lib/browser.js'),
+    },
+    output: {
+      path: path.join(options.baseDir, 'dist'),
+      filename: '[name].min.js',
+    },
+    plugins: makePlugins(options),
+    module: {
+      loaders: [
+        {
+          test: /\.js?$/,
+          exclude: /node_modules/,
+          loader: 'babel',
+        },
+        {
+          test: /\.json?$/,
+          loader: 'json',
+        },
+      ],
+    },
+  };
+}
+
+if (!module.parent) {
+  console.log(makeConfig({
+    isDevelopment: process.env.NODE_ENV !== 'production',
+  }));
+}
+
+exports = module.exports = makeConfig;
+exports.DEFAULTS = DEFAULTS;
-- 
GitLab