From d965fdb11dfba5631c1f98ad3e309876c199999a Mon Sep 17 00:00:00 2001
From: Brandon Millman <brandon.millman@gmail.com>
Date: Tue, 23 Jan 2018 10:10:43 -0800
Subject: Rename to testnet-faucets

---
 packages/kovan-faucets/Dockerfile                  | 13 ---
 packages/kovan-faucets/README.md                   | 78 ------------------
 packages/kovan-faucets/gulpfile.js                 | 92 ---------------------
 packages/kovan-faucets/package.json                | 43 ----------
 packages/kovan-faucets/scripts/postpublish.js      | 15 ----
 packages/kovan-faucets/src/ts/configs.ts           | 12 ---
 packages/kovan-faucets/src/ts/error_reporter.ts    | 40 ----------
 .../kovan-faucets/src/ts/ether_request_queue.ts    | 27 -------
 packages/kovan-faucets/src/ts/global.d.ts          | 26 ------
 packages/kovan-faucets/src/ts/handler.ts           | 93 ----------------------
 packages/kovan-faucets/src/ts/id_management.ts     | 23 ------
 packages/kovan-faucets/src/ts/request_queue.ts     | 56 -------------
 packages/kovan-faucets/src/ts/server.ts            | 28 -------
 packages/kovan-faucets/src/ts/utils.ts             |  7 --
 packages/kovan-faucets/src/ts/zrx_request_queue.ts | 45 -----------
 packages/kovan-faucets/tsconfig.json               |  7 --
 packages/kovan-faucets/tslint.json                 |  3 -
 packages/testnet-faucets/Dockerfile                | 13 +++
 packages/testnet-faucets/README.md                 | 78 ++++++++++++++++++
 packages/testnet-faucets/gulpfile.js               | 92 +++++++++++++++++++++
 packages/testnet-faucets/package.json              | 43 ++++++++++
 packages/testnet-faucets/scripts/postpublish.js    | 15 ++++
 packages/testnet-faucets/src/ts/configs.ts         | 12 +++
 packages/testnet-faucets/src/ts/error_reporter.ts  | 40 ++++++++++
 .../testnet-faucets/src/ts/ether_request_queue.ts  | 27 +++++++
 packages/testnet-faucets/src/ts/global.d.ts        | 26 ++++++
 packages/testnet-faucets/src/ts/handler.ts         | 93 ++++++++++++++++++++++
 packages/testnet-faucets/src/ts/id_management.ts   | 23 ++++++
 packages/testnet-faucets/src/ts/request_queue.ts   | 56 +++++++++++++
 packages/testnet-faucets/src/ts/server.ts          | 28 +++++++
 packages/testnet-faucets/src/ts/utils.ts           |  7 ++
 .../testnet-faucets/src/ts/zrx_request_queue.ts    | 45 +++++++++++
 packages/testnet-faucets/tsconfig.json             |  7 ++
 packages/testnet-faucets/tslint.json               |  3 +
 34 files changed, 608 insertions(+), 608 deletions(-)
 delete mode 100644 packages/kovan-faucets/Dockerfile
 delete mode 100644 packages/kovan-faucets/README.md
 delete mode 100644 packages/kovan-faucets/gulpfile.js
 delete mode 100644 packages/kovan-faucets/package.json
 delete mode 100644 packages/kovan-faucets/scripts/postpublish.js
 delete mode 100644 packages/kovan-faucets/src/ts/configs.ts
 delete mode 100644 packages/kovan-faucets/src/ts/error_reporter.ts
 delete mode 100644 packages/kovan-faucets/src/ts/ether_request_queue.ts
 delete mode 100644 packages/kovan-faucets/src/ts/global.d.ts
 delete mode 100644 packages/kovan-faucets/src/ts/handler.ts
 delete mode 100644 packages/kovan-faucets/src/ts/id_management.ts
 delete mode 100644 packages/kovan-faucets/src/ts/request_queue.ts
 delete mode 100644 packages/kovan-faucets/src/ts/server.ts
 delete mode 100644 packages/kovan-faucets/src/ts/utils.ts
 delete mode 100644 packages/kovan-faucets/src/ts/zrx_request_queue.ts
 delete mode 100644 packages/kovan-faucets/tsconfig.json
 delete mode 100644 packages/kovan-faucets/tslint.json
 create mode 100644 packages/testnet-faucets/Dockerfile
 create mode 100644 packages/testnet-faucets/README.md
 create mode 100644 packages/testnet-faucets/gulpfile.js
 create mode 100644 packages/testnet-faucets/package.json
 create mode 100644 packages/testnet-faucets/scripts/postpublish.js
 create mode 100644 packages/testnet-faucets/src/ts/configs.ts
 create mode 100644 packages/testnet-faucets/src/ts/error_reporter.ts
 create mode 100644 packages/testnet-faucets/src/ts/ether_request_queue.ts
 create mode 100644 packages/testnet-faucets/src/ts/global.d.ts
 create mode 100644 packages/testnet-faucets/src/ts/handler.ts
 create mode 100644 packages/testnet-faucets/src/ts/id_management.ts
 create mode 100644 packages/testnet-faucets/src/ts/request_queue.ts
 create mode 100644 packages/testnet-faucets/src/ts/server.ts
 create mode 100644 packages/testnet-faucets/src/ts/utils.ts
 create mode 100644 packages/testnet-faucets/src/ts/zrx_request_queue.ts
 create mode 100644 packages/testnet-faucets/tsconfig.json
 create mode 100644 packages/testnet-faucets/tslint.json

diff --git a/packages/kovan-faucets/Dockerfile b/packages/kovan-faucets/Dockerfile
deleted file mode 100644
index 6d6ddc192..000000000
--- a/packages/kovan-faucets/Dockerfile
+++ /dev/null
@@ -1,13 +0,0 @@
-FROM node
-
-WORKDIR /src
-
-COPY package.json .
-RUN npm i
-RUN npm install forever -g
-
-COPY . .
-
-EXPOSE 3000
-
-CMD ["forever", "./bin/server.js"]
diff --git a/packages/kovan-faucets/README.md b/packages/kovan-faucets/README.md
deleted file mode 100644
index 07bd23575..000000000
--- a/packages/kovan-faucets/README.md
+++ /dev/null
@@ -1,78 +0,0 @@
-## @0xproject/kovan_faucets
-
-This faucet dispenses 0.1 test ether to one recipient per second and 0.1 test ZRX every 5 seconds. It has a max queue size of 1000.
-
-## Installation
-
-This is a private package and therefore is not published to npm. In order to build and run this package locally, see the [Install Dependencies](#Install-Dependencies) section and onwards below.
-
-## Contributing
-
-We strongly recommend that the community help us make improvements and determine the future direction of the protocol. To report bugs within this package, please create an issue in this repository.
-
-Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting started.
-
-### Install Dependencies
-
-If you don't have yarn workspaces enabled (Yarn < v1.0) - enable them:
-
-```bash
-yarn config set workspaces-experimental true
-```
-
-Then install dependencies
-
-```bash
-yarn install
-```
-
-### Start
-
-Set the following environment variables:
-
-```bash
-export FAUCET_ENVIRONMENT=development
-export DISPENSER_ADDRESS=0x5409ed021d9299bf6814279a6a1411a7e866a631
-export DISPENSER_PRIVATE_KEY=f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d
-export FAUCET_ROLLBAR_ACCESS_KEY={GET_THIS_FROM_ROLLBAR_ACCOUNT_SETTINGS}
-export INFURA_API_KEY={GET_THIS_FROM_INFURA}
-```
-
-Infura API Key can be requested here: https://infura.io/register.html
-
-Note: The above public/private keys exist when running `testrpc` with the following option `--mnemonic concert load couple harbor equip island argue ramp clarify fence smart topic`.
-
-```bash
-yarn dev
-```
-
-### Endpoints
-
-`GET /ether/:recipient`
-
-Where recipient_address is a hex encoded Ethereum address prefixed with `0x`.
-
-`GET /zrx/:recipient`
-
-Where recipient_address is a hex encoded Ethereum address prefixed with `0x`.
-
-### Docker configs
-
-```
-docker run -d \
--p 80:3000 \
---name kovan-faucets \
---log-opt max-size=100m \
---log-opt max-file=20 \
--e DISPENSER_ADDRESS=$DISPENSER_ADDRESS \
--e DISPENSER_PRIVATE_KEY=$DISPENSER_PRIVATE_KEY \
--e FAUCET_ROLLBAR_ACCESS_KEY=$FAUCET_ROLLBAR_ACCESS_KEY \
--e FAUCET_ENVIRONMENT=production \
-kovan-faucets
-```
-
-### Lint
-
-```bash
-yarn lint
-```
diff --git a/packages/kovan-faucets/gulpfile.js b/packages/kovan-faucets/gulpfile.js
deleted file mode 100644
index 773faf33a..000000000
--- a/packages/kovan-faucets/gulpfile.js
+++ /dev/null
@@ -1,92 +0,0 @@
-const gulp = require('gulp');
-const nodemon = require('nodemon');
-const path = require('path');
-const webpack = require('webpack');
-const fs = require('fs');
-const nodeExternals = require('webpack-node-externals');
-
-const config = {
-    target: 'node',
-    entry: [path.join(__dirname, '/src/ts/server.ts')],
-    output: {
-        path: path.join(__dirname, '/bin'),
-        filename: 'server.js',
-    },
-    devtool: 'source-map',
-    resolve: {
-        modules: [
-            path.join(__dirname, '/src/ts'),
-            'node_modules',
-        ],
-        extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
-        alias: {
-            ts: path.join(__dirname, '/src/ts'),
-            contract_artifacts: path.join(__dirname, '/src/contract_artifacts'),
-        },
-    },
-    module: {
-        rules: [
-            {
-                test: /\.js$/,
-                loader: 'source-map-loader',
-            },
-            {
-                test: /\.tsx?$/,
-                loader: 'awesome-typescript-loader',
-            },
-        ],
-    },
-    plugins: [
-        new webpack.BannerPlugin({
-            banner: 'require("source-map-support").install();',
-            raw: true,
-            entryOnly: false,
-        }),
-    ],
-    externals: nodeExternals({
-        modulesDir: path.join(__dirname, '../../node_modules')
-    }),
-    watchOptions: {
-        ignored: /bin|node_modules|transpiled/
-    },
-};
-
-gulp.task('build', function(done) {
-    webpack(config).run(onBuild(done));
-});
-
-gulp.task('watch', function() {
-    webpack(config).watch(100, function(err, stats) {
-        onBuild()(err, stats);
-        nodemon.restart();
-    });
-});
-
-gulp.task('run', ['watch'], function() {
-    nodemon({
-        execMap: {
-            js: 'node',
-        },
-        script: path.join(__dirname, 'bin/server'),
-        ignore: ['*'],
-        watch: ['foo/'],
-        ext: 'noop',
-    }).on('restart', function() {
-        console.log('Restarted!');
-    });
-});
-
-function onBuild(done) {
-  return function(err, stats) {
-    if(err) {
-      console.log('Error', err);
-    }
-    else {
-      console.log(stats.toString());
-    }
-
-    if(done) {
-      done();
-    }
-  }
-}
diff --git a/packages/kovan-faucets/package.json b/packages/kovan-faucets/package.json
deleted file mode 100644
index eab0dc677..000000000
--- a/packages/kovan-faucets/package.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
-    "private": true,
-    "name": "@0xproject/kovan_faucets",
-    "version": "1.0.5",
-    "description": "A faucet micro-service that dispenses test ERC20 tokens or Ether",
-    "main": "server.js",
-    "scripts": {
-        "build": "node ../../node_modules/gulp/bin/gulp.js build",
-        "dev": "node ../../node_modules/gulp/bin/gulp.js run",
-        "start": "node ./bin/server.js",
-        "lint": "tslint --project . 'src/**/*.ts'",
-        "clean": "shx rm -rf bin"
-    },
-    "author": "Fabio Berger",
-    "license": "Apache-2.0",
-    "dependencies": {
-        "0x.js": "^0.30.0",
-        "@0xproject/utils": "^0.2.0",
-        "body-parser": "^1.17.1",
-        "ethereumjs-tx": "^1.3.3",
-        "express": "^4.15.2",
-        "lodash": "^4.17.4",
-        "rollbar": "^0.6.5",
-        "web3": "^0.20.0",
-        "web3-provider-engine": "^13.0.1"
-    },
-    "devDependencies": {
-        "@0xproject/tslint-config": "^0.4.2",
-        "@types/body-parser": "^1.16.1",
-        "@types/express": "^4.0.35",
-        "@types/lodash": "^4.14.86",
-        "awesome-typescript-loader": "^3.1.3",
-        "gulp": "^3.9.1",
-        "nodemon": "^1.11.0",
-        "shx": "^0.2.2",
-        "source-map-loader": "^0.1.6",
-        "tslint": "5.8.0",
-        "typescript": "~2.6.1",
-        "web3-typescript-typings": "^0.9.4",
-        "webpack": "^3.1.0",
-        "webpack-node-externals": "^1.6.0"
-    }
-}
diff --git a/packages/kovan-faucets/scripts/postpublish.js b/packages/kovan-faucets/scripts/postpublish.js
deleted file mode 100644
index 16d67e03f..000000000
--- a/packages/kovan-faucets/scripts/postpublish.js
+++ /dev/null
@@ -1,15 +0,0 @@
-const postpublish_utils = require('../../../scripts/postpublish_utils');
-const packageJSON = require('../package.json');
-
-const subPackageName = packageJSON.name;
-
-postpublish_utils
-    .getLatestTagAndVersionAsync(subPackageName)
-    .then(function(result) {
-        const releaseName = postpublish_utils.getReleaseName(subPackageName, result.version);
-        const assets = [];
-        return postpublish_utils.publishReleaseNotes(result.tag, releaseName, assets);
-    })
-    .catch(function(err) {
-        throw err;
-    });
diff --git a/packages/kovan-faucets/src/ts/configs.ts b/packages/kovan-faucets/src/ts/configs.ts
deleted file mode 100644
index 2e5a7f64d..000000000
--- a/packages/kovan-faucets/src/ts/configs.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export const configs = {
-    DISPENSER_ADDRESS: (process.env.DISPENSER_ADDRESS as string).toLowerCase(),
-    DISPENSER_PRIVATE_KEY: process.env.DISPENSER_PRIVATE_KEY,
-    ENVIRONMENT: process.env.FAUCET_ENVIRONMENT,
-    ROLLBAR_ACCESS_KEY: process.env.FAUCET_ROLLBAR_ACCESS_KEY,
-    RPC_URL:
-        process.env.FAUCET_ENVIRONMENT === 'development'
-            ? 'http://127.0.0.1:8545'
-            : `https://kovan.infura.io/${process.env.INFURA_API_KEY}`,
-    ZRX_TOKEN_ADDRESS: '0x6ff6c0ff1d68b964901f986d4c9fa3ac68346570',
-    KOVAN_NETWORK_ID: 42,
-};
diff --git a/packages/kovan-faucets/src/ts/error_reporter.ts b/packages/kovan-faucets/src/ts/error_reporter.ts
deleted file mode 100644
index 6865d3893..000000000
--- a/packages/kovan-faucets/src/ts/error_reporter.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import * as express from 'express';
-import rollbar = require('rollbar');
-
-import { configs } from './configs';
-import { utils } from './utils';
-
-export const errorReporter = {
-    setup() {
-        rollbar.init(configs.ROLLBAR_ACCESS_KEY, {
-            environment: configs.ENVIRONMENT,
-        });
-
-        rollbar.handleUncaughtExceptions(configs.ROLLBAR_ACCESS_KEY);
-
-        process.on('unhandledRejection', async (err: Error) => {
-            utils.consoleLog(`Uncaught exception ${err}. Stack: ${err.stack}`);
-            await this.reportAsync(err);
-            process.exit(1);
-        });
-    },
-    async reportAsync(err: Error, req?: express.Request): Promise<any> {
-        if (configs.ENVIRONMENT === 'development') {
-            return; // Do not log development environment errors
-        }
-
-        return new Promise((resolve, reject) => {
-            rollbar.handleError(err, req, (rollbarErr: Error) => {
-                if (rollbarErr) {
-                    utils.consoleLog(`Error reporting to rollbar, ignoring: ${rollbarErr}`);
-                    reject(rollbarErr);
-                } else {
-                    resolve();
-                }
-            });
-        });
-    },
-    errorHandler() {
-        return rollbar.errorHandler(configs.ROLLBAR_ACCESS_KEY);
-    },
-};
diff --git a/packages/kovan-faucets/src/ts/ether_request_queue.ts b/packages/kovan-faucets/src/ts/ether_request_queue.ts
deleted file mode 100644
index 1c4b19ab9..000000000
--- a/packages/kovan-faucets/src/ts/ether_request_queue.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { promisify } from '@0xproject/utils';
-import * as _ from 'lodash';
-
-import { configs } from './configs';
-import { errorReporter } from './error_reporter';
-import { RequestQueue } from './request_queue';
-import { utils } from './utils';
-
-const DISPENSE_AMOUNT_ETHER = 0.1;
-
-export class EtherRequestQueue extends RequestQueue {
-    protected async processNextRequestFireAndForgetAsync(recipientAddress: string) {
-        utils.consoleLog(`Processing ETH ${recipientAddress}`);
-        const sendTransactionAsync = promisify(this.web3.eth.sendTransaction);
-        try {
-            const txHash = await sendTransactionAsync({
-                from: configs.DISPENSER_ADDRESS,
-                to: recipientAddress,
-                value: this.web3.toWei(DISPENSE_AMOUNT_ETHER, 'ether'),
-            });
-            utils.consoleLog(`Sent ${DISPENSE_AMOUNT_ETHER} ETH to ${recipientAddress} tx: ${txHash}`);
-        } catch (err) {
-            utils.consoleLog(`Unexpected err: ${err} - ${JSON.stringify(err)}`);
-            await errorReporter.reportAsync(err);
-        }
-    }
-}
diff --git a/packages/kovan-faucets/src/ts/global.d.ts b/packages/kovan-faucets/src/ts/global.d.ts
deleted file mode 100644
index 97cd35680..000000000
--- a/packages/kovan-faucets/src/ts/global.d.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-declare module 'rollbar';
-declare module 'web3-provider-engine';
-declare module 'web3-provider-engine/subproviders/rpc';
-declare module 'web3-provider-engine/subproviders/nonce-tracker';
-declare module 'web3-provider-engine/subproviders/hooked-wallet';
-
-declare module '*.json' {
-    const json: any;
-    /* tslint:disable */
-    export default json;
-    /* tslint:enable */
-}
-
-// Ethereumjs-tx declarations
-declare module 'ethereumjs-tx' {
-    class EthereumTx {
-        public raw: Buffer[];
-        public r: Buffer;
-        public s: Buffer;
-        public v: Buffer;
-        public serialize(): Buffer;
-        public sign(buffer: Buffer): void;
-        constructor(txParams: any);
-    }
-    export = EthereumTx;
-}
diff --git a/packages/kovan-faucets/src/ts/handler.ts b/packages/kovan-faucets/src/ts/handler.ts
deleted file mode 100644
index 4bf776264..000000000
--- a/packages/kovan-faucets/src/ts/handler.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import * as express from 'express';
-import * as _ from 'lodash';
-import ProviderEngine = require('web3-provider-engine');
-import HookedWalletSubprovider = require('web3-provider-engine/subproviders/hooked-wallet');
-import NonceSubprovider = require('web3-provider-engine/subproviders/nonce-tracker');
-import RpcSubprovider = require('web3-provider-engine/subproviders/rpc');
-
-import { configs } from './configs';
-import { EtherRequestQueue } from './ether_request_queue';
-import { idManagement } from './id_management';
-import { utils } from './utils';
-import { ZRXRequestQueue } from './zrx_request_queue';
-
-// HACK: web3 leaks XMLHttpRequest into the global scope and causes requests to hang
-// because they are using the wrong XHR package.
-// Filed issue: https://github.com/ethereum/web3.js/issues/844
-// tslint:disable-next-line:ordered-imports
-import * as Web3 from 'web3';
-
-export class Handler {
-    private _etherRequestQueue: EtherRequestQueue;
-    private _zrxRequestQueue: ZRXRequestQueue;
-    private _web3: Web3;
-    constructor() {
-        // Setup provider engine to talk with RPC node
-        const providerObj = this._createProviderEngine(configs.RPC_URL);
-        this._web3 = new Web3(providerObj);
-
-        this._etherRequestQueue = new EtherRequestQueue(this._web3);
-        this._zrxRequestQueue = new ZRXRequestQueue(this._web3);
-    }
-    public dispenseEther(req: express.Request, res: express.Response) {
-        const recipientAddress = req.params.recipient;
-        if (_.isUndefined(recipientAddress) || !this._isValidEthereumAddress(recipientAddress)) {
-            res.status(400).send('INVALID_REQUEST');
-            return;
-        }
-        const lowerCaseRecipientAddress = recipientAddress.toLowerCase();
-        const didAddToQueue = this._etherRequestQueue.add(lowerCaseRecipientAddress);
-        if (!didAddToQueue) {
-            res.status(503).send('QUEUE_IS_FULL');
-            return;
-        }
-        utils.consoleLog(`Added ${lowerCaseRecipientAddress} to the ETH queue`);
-        res.status(200).end();
-    }
-    public dispenseZRX(req: express.Request, res: express.Response) {
-        const recipientAddress = req.params.recipient;
-        if (_.isUndefined(recipientAddress) || !this._isValidEthereumAddress(recipientAddress)) {
-            res.status(400).send('INVALID_REQUEST');
-            return;
-        }
-        const lowerCaseRecipientAddress = recipientAddress.toLowerCase();
-        const didAddToQueue = this._zrxRequestQueue.add(lowerCaseRecipientAddress);
-        if (!didAddToQueue) {
-            res.status(503).send('QUEUE_IS_FULL');
-            return;
-        }
-        utils.consoleLog(`Added ${lowerCaseRecipientAddress} to the ZRX queue`);
-        res.status(200).end();
-    }
-    public getQueueInfo(req: express.Request, res: express.Response) {
-        res.setHeader('Content-Type', 'application/json');
-        const payload = JSON.stringify({
-            ether: {
-                full: this._etherRequestQueue.isFull(),
-                size: this._etherRequestQueue.size(),
-            },
-            zrx: {
-                full: this._zrxRequestQueue.isFull(),
-                size: this._zrxRequestQueue.size(),
-            },
-        });
-        res.status(200).send(payload);
-    }
-    // tslint:disable-next-line:prefer-function-over-method
-    private _createProviderEngine(rpcUrl: string) {
-        const engine = new ProviderEngine();
-        engine.addProvider(new NonceSubprovider());
-        engine.addProvider(new HookedWalletSubprovider(idManagement));
-        engine.addProvider(
-            new RpcSubprovider({
-                rpcUrl,
-            }),
-        );
-        engine.start();
-        return engine;
-    }
-    private _isValidEthereumAddress(address: string): boolean {
-        const lowercaseAddress = address.toLowerCase();
-        return this._web3.isAddress(lowercaseAddress);
-    }
-}
diff --git a/packages/kovan-faucets/src/ts/id_management.ts b/packages/kovan-faucets/src/ts/id_management.ts
deleted file mode 100644
index 930821172..000000000
--- a/packages/kovan-faucets/src/ts/id_management.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import EthereumTx = require('ethereumjs-tx');
-
-import { configs } from './configs';
-import { utils } from './utils';
-
-type Callback = (err: Error | null, accounts: any) => void;
-
-export const idManagement = {
-    getAccounts(callback: Callback) {
-        utils.consoleLog(`configs.DISPENSER_ADDRESS: ${configs.DISPENSER_ADDRESS}`);
-        callback(null, [configs.DISPENSER_ADDRESS]);
-    },
-    approveTransaction(txData: object, callback: Callback) {
-        callback(null, true);
-    },
-    signTransaction(txData: object, callback: Callback) {
-        const tx = new EthereumTx(txData);
-        const privateKeyBuffer = new Buffer(configs.DISPENSER_PRIVATE_KEY as string, 'hex');
-        tx.sign(privateKeyBuffer);
-        const rawTx = `0x${tx.serialize().toString('hex')}`;
-        callback(null, rawTx);
-    },
-};
diff --git a/packages/kovan-faucets/src/ts/request_queue.ts b/packages/kovan-faucets/src/ts/request_queue.ts
deleted file mode 100644
index 2b42ca4bf..000000000
--- a/packages/kovan-faucets/src/ts/request_queue.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import * as _ from 'lodash';
-import * as timers from 'timers';
-
-// HACK: web3 leaks XMLHttpRequest into the global scope and causes requests to hang
-// because they are using the wrong XHR package.
-// Filed issue: https://github.com/ethereum/web3.js/issues/844
-// tslint:disable-next-line:ordered-imports
-import * as Web3 from 'web3';
-
-const MAX_QUEUE_SIZE = 500;
-const DEFAULT_QUEUE_INTERVAL_MS = 1000;
-
-export class RequestQueue {
-    protected queueIntervalMs: number;
-    protected queue: string[];
-    protected queueIntervalId: NodeJS.Timer;
-    protected web3: Web3;
-    constructor(web3: any) {
-        this.queueIntervalMs = DEFAULT_QUEUE_INTERVAL_MS;
-        this.queue = [];
-
-        this.web3 = web3;
-
-        this.start();
-    }
-    public add(recipientAddress: string): boolean {
-        if (this.isFull()) {
-            return false;
-        }
-        this.queue.push(recipientAddress);
-        return true;
-    }
-    public size(): number {
-        return this.queue.length;
-    }
-    public isFull(): boolean {
-        return this.size() >= MAX_QUEUE_SIZE;
-    }
-    protected start() {
-        this.queueIntervalId = timers.setInterval(() => {
-            const recipientAddress = this.queue.shift();
-            if (_.isUndefined(recipientAddress)) {
-                return;
-            }
-            // tslint:disable-next-line:no-floating-promises
-            this.processNextRequestFireAndForgetAsync(recipientAddress);
-        }, this.queueIntervalMs);
-    }
-    protected stop() {
-        clearInterval(this.queueIntervalId);
-    }
-    // tslint:disable-next-line:prefer-function-over-method
-    protected async processNextRequestFireAndForgetAsync(recipientAddress: string) {
-        throw new Error('Expected processNextRequestFireAndForgetAsync to be implemented by a superclass');
-    }
-}
diff --git a/packages/kovan-faucets/src/ts/server.ts b/packages/kovan-faucets/src/ts/server.ts
deleted file mode 100644
index 23642787d..000000000
--- a/packages/kovan-faucets/src/ts/server.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import * as bodyParser from 'body-parser';
-import * as express from 'express';
-
-import { errorReporter } from './error_reporter';
-import { Handler } from './handler';
-
-// Setup the errorReporter to catch uncaught exceptions and unhandled rejections
-errorReporter.setup();
-
-const app = express();
-app.use(bodyParser.json()); // for parsing application/json
-app.use((req, res, next) => {
-    res.header('Access-Control-Allow-Origin', '*');
-    res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
-    next();
-});
-
-const handler = new Handler();
-app.get('/ping', (req: express.Request, res: express.Response) => {
-    res.status(200).send('pong');
-});
-app.get('/ether/:recipient', handler.dispenseEther.bind(handler));
-app.get('/zrx/:recipient', handler.dispenseZRX.bind(handler));
-
-// Log to rollbar any errors unhandled by handlers
-app.use(errorReporter.errorHandler());
-const port = process.env.PORT || 3000;
-app.listen(port);
diff --git a/packages/kovan-faucets/src/ts/utils.ts b/packages/kovan-faucets/src/ts/utils.ts
deleted file mode 100644
index 893f82ca3..000000000
--- a/packages/kovan-faucets/src/ts/utils.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export const utils = {
-    consoleLog(message: string) {
-        /* tslint:disable */
-        console.log(message);
-        /* tslint:enable */
-    },
-};
diff --git a/packages/kovan-faucets/src/ts/zrx_request_queue.ts b/packages/kovan-faucets/src/ts/zrx_request_queue.ts
deleted file mode 100644
index bbc06f1de..000000000
--- a/packages/kovan-faucets/src/ts/zrx_request_queue.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { ZeroEx } from '0x.js';
-import { BigNumber } from '@0xproject/utils';
-import * as _ from 'lodash';
-
-import { configs } from './configs';
-import { errorReporter } from './error_reporter';
-import { RequestQueue } from './request_queue';
-import { utils } from './utils';
-
-// HACK: web3 leaks XMLHttpRequest into the global scope and causes requests to hang
-// because they are using the wrong XHR package.
-// Filed issue: https://github.com/ethereum/web3.js/issues/844
-// tslint:disable-next-line:ordered-imports
-import * as Web3 from 'web3';
-
-const DISPENSE_AMOUNT_ZRX = new BigNumber(0.1);
-const QUEUE_INTERVAL_MS = 5000;
-
-export class ZRXRequestQueue extends RequestQueue {
-    private _zeroEx: ZeroEx;
-    constructor(web3: Web3) {
-        super(web3);
-        this.queueIntervalMs = QUEUE_INTERVAL_MS;
-        const zeroExConfig = {
-            networkId: configs.KOVAN_NETWORK_ID,
-        };
-        this._zeroEx = new ZeroEx(web3.currentProvider, zeroExConfig);
-    }
-    protected async processNextRequestFireAndForgetAsync(recipientAddress: string) {
-        utils.consoleLog(`Processing ZRX ${recipientAddress}`);
-        const baseUnitAmount = ZeroEx.toBaseUnitAmount(DISPENSE_AMOUNT_ZRX, 18);
-        try {
-            await this._zeroEx.token.transferAsync(
-                configs.ZRX_TOKEN_ADDRESS,
-                configs.DISPENSER_ADDRESS,
-                recipientAddress,
-                baseUnitAmount,
-            );
-            utils.consoleLog(`Sent ${DISPENSE_AMOUNT_ZRX} ZRX to ${recipientAddress}`);
-        } catch (err) {
-            utils.consoleLog(`Unexpected err: ${err} - ${JSON.stringify(err)}`);
-            await errorReporter.reportAsync(err);
-        }
-    }
-}
diff --git a/packages/kovan-faucets/tsconfig.json b/packages/kovan-faucets/tsconfig.json
deleted file mode 100644
index 7f0c084ff..000000000
--- a/packages/kovan-faucets/tsconfig.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-    "extends": "../../tsconfig",
-    "compilerOptions": {
-        "outDir": "lib"
-    },
-    "include": ["./src/ts/**/*", "../../node_modules/web3-typescript-typings/index.d.ts"]
-}
diff --git a/packages/kovan-faucets/tslint.json b/packages/kovan-faucets/tslint.json
deleted file mode 100644
index ffaefe83a..000000000
--- a/packages/kovan-faucets/tslint.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-    "extends": ["@0xproject/tslint-config"]
-}
diff --git a/packages/testnet-faucets/Dockerfile b/packages/testnet-faucets/Dockerfile
new file mode 100644
index 000000000..6d6ddc192
--- /dev/null
+++ b/packages/testnet-faucets/Dockerfile
@@ -0,0 +1,13 @@
+FROM node
+
+WORKDIR /src
+
+COPY package.json .
+RUN npm i
+RUN npm install forever -g
+
+COPY . .
+
+EXPOSE 3000
+
+CMD ["forever", "./bin/server.js"]
diff --git a/packages/testnet-faucets/README.md b/packages/testnet-faucets/README.md
new file mode 100644
index 000000000..504eacedc
--- /dev/null
+++ b/packages/testnet-faucets/README.md
@@ -0,0 +1,78 @@
+## @0xproject/testnet_faucets
+
+This faucet dispenses 0.1 test ether to one recipient per second and 0.1 test ZRX every 5 seconds. It has a max queue size of 1000.
+
+## Installation
+
+This is a private package and therefore is not published to npm. In order to build and run this package locally, see the [Install Dependencies](#Install-Dependencies) section and onwards below.
+
+## Contributing
+
+We strongly recommend that the community help us make improvements and determine the future direction of the protocol. To report bugs within this package, please create an issue in this repository.
+
+Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting started.
+
+### Install Dependencies
+
+If you don't have yarn workspaces enabled (Yarn < v1.0) - enable them:
+
+```bash
+yarn config set workspaces-experimental true
+```
+
+Then install dependencies
+
+```bash
+yarn install
+```
+
+### Start
+
+Set the following environment variables:
+
+```bash
+export FAUCET_ENVIRONMENT=development
+export DISPENSER_ADDRESS=0x5409ed021d9299bf6814279a6a1411a7e866a631
+export DISPENSER_PRIVATE_KEY=f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d
+export FAUCET_ROLLBAR_ACCESS_KEY={GET_THIS_FROM_ROLLBAR_ACCOUNT_SETTINGS}
+export INFURA_API_KEY={GET_THIS_FROM_INFURA}
+```
+
+Infura API Key can be requested here: https://infura.io/register.html
+
+Note: The above public/private keys exist when running `testrpc` with the following option `--mnemonic concert load couple harbor equip island argue ramp clarify fence smart topic`.
+
+```bash
+yarn dev
+```
+
+### Endpoints
+
+`GET /ether/:recipient`
+
+Where recipient_address is a hex encoded Ethereum address prefixed with `0x`.
+
+`GET /zrx/:recipient`
+
+Where recipient_address is a hex encoded Ethereum address prefixed with `0x`.
+
+### Docker configs
+
+```
+docker run -d \
+-p 80:3000 \
+--name testnet-faucets \
+--log-opt max-size=100m \
+--log-opt max-file=20 \
+-e DISPENSER_ADDRESS=$DISPENSER_ADDRESS \
+-e DISPENSER_PRIVATE_KEY=$DISPENSER_PRIVATE_KEY \
+-e FAUCET_ROLLBAR_ACCESS_KEY=$FAUCET_ROLLBAR_ACCESS_KEY \
+-e FAUCET_ENVIRONMENT=production \
+testnet-faucets
+```
+
+### Lint
+
+```bash
+yarn lint
+```
diff --git a/packages/testnet-faucets/gulpfile.js b/packages/testnet-faucets/gulpfile.js
new file mode 100644
index 000000000..773faf33a
--- /dev/null
+++ b/packages/testnet-faucets/gulpfile.js
@@ -0,0 +1,92 @@
+const gulp = require('gulp');
+const nodemon = require('nodemon');
+const path = require('path');
+const webpack = require('webpack');
+const fs = require('fs');
+const nodeExternals = require('webpack-node-externals');
+
+const config = {
+    target: 'node',
+    entry: [path.join(__dirname, '/src/ts/server.ts')],
+    output: {
+        path: path.join(__dirname, '/bin'),
+        filename: 'server.js',
+    },
+    devtool: 'source-map',
+    resolve: {
+        modules: [
+            path.join(__dirname, '/src/ts'),
+            'node_modules',
+        ],
+        extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
+        alias: {
+            ts: path.join(__dirname, '/src/ts'),
+            contract_artifacts: path.join(__dirname, '/src/contract_artifacts'),
+        },
+    },
+    module: {
+        rules: [
+            {
+                test: /\.js$/,
+                loader: 'source-map-loader',
+            },
+            {
+                test: /\.tsx?$/,
+                loader: 'awesome-typescript-loader',
+            },
+        ],
+    },
+    plugins: [
+        new webpack.BannerPlugin({
+            banner: 'require("source-map-support").install();',
+            raw: true,
+            entryOnly: false,
+        }),
+    ],
+    externals: nodeExternals({
+        modulesDir: path.join(__dirname, '../../node_modules')
+    }),
+    watchOptions: {
+        ignored: /bin|node_modules|transpiled/
+    },
+};
+
+gulp.task('build', function(done) {
+    webpack(config).run(onBuild(done));
+});
+
+gulp.task('watch', function() {
+    webpack(config).watch(100, function(err, stats) {
+        onBuild()(err, stats);
+        nodemon.restart();
+    });
+});
+
+gulp.task('run', ['watch'], function() {
+    nodemon({
+        execMap: {
+            js: 'node',
+        },
+        script: path.join(__dirname, 'bin/server'),
+        ignore: ['*'],
+        watch: ['foo/'],
+        ext: 'noop',
+    }).on('restart', function() {
+        console.log('Restarted!');
+    });
+});
+
+function onBuild(done) {
+  return function(err, stats) {
+    if(err) {
+      console.log('Error', err);
+    }
+    else {
+      console.log(stats.toString());
+    }
+
+    if(done) {
+      done();
+    }
+  }
+}
diff --git a/packages/testnet-faucets/package.json b/packages/testnet-faucets/package.json
new file mode 100644
index 000000000..3f5698332
--- /dev/null
+++ b/packages/testnet-faucets/package.json
@@ -0,0 +1,43 @@
+{
+    "private": true,
+    "name": "@0xproject/testnet_faucets",
+    "version": "1.0.5",
+    "description": "A faucet micro-service that dispenses test ERC20 tokens or Ether",
+    "main": "server.js",
+    "scripts": {
+        "build": "node ../../node_modules/gulp/bin/gulp.js build",
+        "dev": "node ../../node_modules/gulp/bin/gulp.js run",
+        "start": "node ./bin/server.js",
+        "lint": "tslint --project . 'src/**/*.ts'",
+        "clean": "shx rm -rf bin"
+    },
+    "author": "Fabio Berger",
+    "license": "Apache-2.0",
+    "dependencies": {
+        "0x.js": "^0.30.0",
+        "@0xproject/utils": "^0.2.0",
+        "body-parser": "^1.17.1",
+        "ethereumjs-tx": "^1.3.3",
+        "express": "^4.15.2",
+        "lodash": "^4.17.4",
+        "rollbar": "^0.6.5",
+        "web3": "^0.20.0",
+        "web3-provider-engine": "^13.0.1"
+    },
+    "devDependencies": {
+        "@0xproject/tslint-config": "^0.4.2",
+        "@types/body-parser": "^1.16.1",
+        "@types/express": "^4.0.35",
+        "@types/lodash": "^4.14.86",
+        "awesome-typescript-loader": "^3.1.3",
+        "gulp": "^3.9.1",
+        "nodemon": "^1.11.0",
+        "shx": "^0.2.2",
+        "source-map-loader": "^0.1.6",
+        "tslint": "5.8.0",
+        "typescript": "~2.6.1",
+        "web3-typescript-typings": "^0.9.4",
+        "webpack": "^3.1.0",
+        "webpack-node-externals": "^1.6.0"
+    }
+}
diff --git a/packages/testnet-faucets/scripts/postpublish.js b/packages/testnet-faucets/scripts/postpublish.js
new file mode 100644
index 000000000..16d67e03f
--- /dev/null
+++ b/packages/testnet-faucets/scripts/postpublish.js
@@ -0,0 +1,15 @@
+const postpublish_utils = require('../../../scripts/postpublish_utils');
+const packageJSON = require('../package.json');
+
+const subPackageName = packageJSON.name;
+
+postpublish_utils
+    .getLatestTagAndVersionAsync(subPackageName)
+    .then(function(result) {
+        const releaseName = postpublish_utils.getReleaseName(subPackageName, result.version);
+        const assets = [];
+        return postpublish_utils.publishReleaseNotes(result.tag, releaseName, assets);
+    })
+    .catch(function(err) {
+        throw err;
+    });
diff --git a/packages/testnet-faucets/src/ts/configs.ts b/packages/testnet-faucets/src/ts/configs.ts
new file mode 100644
index 000000000..2e5a7f64d
--- /dev/null
+++ b/packages/testnet-faucets/src/ts/configs.ts
@@ -0,0 +1,12 @@
+export const configs = {
+    DISPENSER_ADDRESS: (process.env.DISPENSER_ADDRESS as string).toLowerCase(),
+    DISPENSER_PRIVATE_KEY: process.env.DISPENSER_PRIVATE_KEY,
+    ENVIRONMENT: process.env.FAUCET_ENVIRONMENT,
+    ROLLBAR_ACCESS_KEY: process.env.FAUCET_ROLLBAR_ACCESS_KEY,
+    RPC_URL:
+        process.env.FAUCET_ENVIRONMENT === 'development'
+            ? 'http://127.0.0.1:8545'
+            : `https://kovan.infura.io/${process.env.INFURA_API_KEY}`,
+    ZRX_TOKEN_ADDRESS: '0x6ff6c0ff1d68b964901f986d4c9fa3ac68346570',
+    KOVAN_NETWORK_ID: 42,
+};
diff --git a/packages/testnet-faucets/src/ts/error_reporter.ts b/packages/testnet-faucets/src/ts/error_reporter.ts
new file mode 100644
index 000000000..6865d3893
--- /dev/null
+++ b/packages/testnet-faucets/src/ts/error_reporter.ts
@@ -0,0 +1,40 @@
+import * as express from 'express';
+import rollbar = require('rollbar');
+
+import { configs } from './configs';
+import { utils } from './utils';
+
+export const errorReporter = {
+    setup() {
+        rollbar.init(configs.ROLLBAR_ACCESS_KEY, {
+            environment: configs.ENVIRONMENT,
+        });
+
+        rollbar.handleUncaughtExceptions(configs.ROLLBAR_ACCESS_KEY);
+
+        process.on('unhandledRejection', async (err: Error) => {
+            utils.consoleLog(`Uncaught exception ${err}. Stack: ${err.stack}`);
+            await this.reportAsync(err);
+            process.exit(1);
+        });
+    },
+    async reportAsync(err: Error, req?: express.Request): Promise<any> {
+        if (configs.ENVIRONMENT === 'development') {
+            return; // Do not log development environment errors
+        }
+
+        return new Promise((resolve, reject) => {
+            rollbar.handleError(err, req, (rollbarErr: Error) => {
+                if (rollbarErr) {
+                    utils.consoleLog(`Error reporting to rollbar, ignoring: ${rollbarErr}`);
+                    reject(rollbarErr);
+                } else {
+                    resolve();
+                }
+            });
+        });
+    },
+    errorHandler() {
+        return rollbar.errorHandler(configs.ROLLBAR_ACCESS_KEY);
+    },
+};
diff --git a/packages/testnet-faucets/src/ts/ether_request_queue.ts b/packages/testnet-faucets/src/ts/ether_request_queue.ts
new file mode 100644
index 000000000..1c4b19ab9
--- /dev/null
+++ b/packages/testnet-faucets/src/ts/ether_request_queue.ts
@@ -0,0 +1,27 @@
+import { promisify } from '@0xproject/utils';
+import * as _ from 'lodash';
+
+import { configs } from './configs';
+import { errorReporter } from './error_reporter';
+import { RequestQueue } from './request_queue';
+import { utils } from './utils';
+
+const DISPENSE_AMOUNT_ETHER = 0.1;
+
+export class EtherRequestQueue extends RequestQueue {
+    protected async processNextRequestFireAndForgetAsync(recipientAddress: string) {
+        utils.consoleLog(`Processing ETH ${recipientAddress}`);
+        const sendTransactionAsync = promisify(this.web3.eth.sendTransaction);
+        try {
+            const txHash = await sendTransactionAsync({
+                from: configs.DISPENSER_ADDRESS,
+                to: recipientAddress,
+                value: this.web3.toWei(DISPENSE_AMOUNT_ETHER, 'ether'),
+            });
+            utils.consoleLog(`Sent ${DISPENSE_AMOUNT_ETHER} ETH to ${recipientAddress} tx: ${txHash}`);
+        } catch (err) {
+            utils.consoleLog(`Unexpected err: ${err} - ${JSON.stringify(err)}`);
+            await errorReporter.reportAsync(err);
+        }
+    }
+}
diff --git a/packages/testnet-faucets/src/ts/global.d.ts b/packages/testnet-faucets/src/ts/global.d.ts
new file mode 100644
index 000000000..97cd35680
--- /dev/null
+++ b/packages/testnet-faucets/src/ts/global.d.ts
@@ -0,0 +1,26 @@
+declare module 'rollbar';
+declare module 'web3-provider-engine';
+declare module 'web3-provider-engine/subproviders/rpc';
+declare module 'web3-provider-engine/subproviders/nonce-tracker';
+declare module 'web3-provider-engine/subproviders/hooked-wallet';
+
+declare module '*.json' {
+    const json: any;
+    /* tslint:disable */
+    export default json;
+    /* tslint:enable */
+}
+
+// Ethereumjs-tx declarations
+declare module 'ethereumjs-tx' {
+    class EthereumTx {
+        public raw: Buffer[];
+        public r: Buffer;
+        public s: Buffer;
+        public v: Buffer;
+        public serialize(): Buffer;
+        public sign(buffer: Buffer): void;
+        constructor(txParams: any);
+    }
+    export = EthereumTx;
+}
diff --git a/packages/testnet-faucets/src/ts/handler.ts b/packages/testnet-faucets/src/ts/handler.ts
new file mode 100644
index 000000000..4bf776264
--- /dev/null
+++ b/packages/testnet-faucets/src/ts/handler.ts
@@ -0,0 +1,93 @@
+import * as express from 'express';
+import * as _ from 'lodash';
+import ProviderEngine = require('web3-provider-engine');
+import HookedWalletSubprovider = require('web3-provider-engine/subproviders/hooked-wallet');
+import NonceSubprovider = require('web3-provider-engine/subproviders/nonce-tracker');
+import RpcSubprovider = require('web3-provider-engine/subproviders/rpc');
+
+import { configs } from './configs';
+import { EtherRequestQueue } from './ether_request_queue';
+import { idManagement } from './id_management';
+import { utils } from './utils';
+import { ZRXRequestQueue } from './zrx_request_queue';
+
+// HACK: web3 leaks XMLHttpRequest into the global scope and causes requests to hang
+// because they are using the wrong XHR package.
+// Filed issue: https://github.com/ethereum/web3.js/issues/844
+// tslint:disable-next-line:ordered-imports
+import * as Web3 from 'web3';
+
+export class Handler {
+    private _etherRequestQueue: EtherRequestQueue;
+    private _zrxRequestQueue: ZRXRequestQueue;
+    private _web3: Web3;
+    constructor() {
+        // Setup provider engine to talk with RPC node
+        const providerObj = this._createProviderEngine(configs.RPC_URL);
+        this._web3 = new Web3(providerObj);
+
+        this._etherRequestQueue = new EtherRequestQueue(this._web3);
+        this._zrxRequestQueue = new ZRXRequestQueue(this._web3);
+    }
+    public dispenseEther(req: express.Request, res: express.Response) {
+        const recipientAddress = req.params.recipient;
+        if (_.isUndefined(recipientAddress) || !this._isValidEthereumAddress(recipientAddress)) {
+            res.status(400).send('INVALID_REQUEST');
+            return;
+        }
+        const lowerCaseRecipientAddress = recipientAddress.toLowerCase();
+        const didAddToQueue = this._etherRequestQueue.add(lowerCaseRecipientAddress);
+        if (!didAddToQueue) {
+            res.status(503).send('QUEUE_IS_FULL');
+            return;
+        }
+        utils.consoleLog(`Added ${lowerCaseRecipientAddress} to the ETH queue`);
+        res.status(200).end();
+    }
+    public dispenseZRX(req: express.Request, res: express.Response) {
+        const recipientAddress = req.params.recipient;
+        if (_.isUndefined(recipientAddress) || !this._isValidEthereumAddress(recipientAddress)) {
+            res.status(400).send('INVALID_REQUEST');
+            return;
+        }
+        const lowerCaseRecipientAddress = recipientAddress.toLowerCase();
+        const didAddToQueue = this._zrxRequestQueue.add(lowerCaseRecipientAddress);
+        if (!didAddToQueue) {
+            res.status(503).send('QUEUE_IS_FULL');
+            return;
+        }
+        utils.consoleLog(`Added ${lowerCaseRecipientAddress} to the ZRX queue`);
+        res.status(200).end();
+    }
+    public getQueueInfo(req: express.Request, res: express.Response) {
+        res.setHeader('Content-Type', 'application/json');
+        const payload = JSON.stringify({
+            ether: {
+                full: this._etherRequestQueue.isFull(),
+                size: this._etherRequestQueue.size(),
+            },
+            zrx: {
+                full: this._zrxRequestQueue.isFull(),
+                size: this._zrxRequestQueue.size(),
+            },
+        });
+        res.status(200).send(payload);
+    }
+    // tslint:disable-next-line:prefer-function-over-method
+    private _createProviderEngine(rpcUrl: string) {
+        const engine = new ProviderEngine();
+        engine.addProvider(new NonceSubprovider());
+        engine.addProvider(new HookedWalletSubprovider(idManagement));
+        engine.addProvider(
+            new RpcSubprovider({
+                rpcUrl,
+            }),
+        );
+        engine.start();
+        return engine;
+    }
+    private _isValidEthereumAddress(address: string): boolean {
+        const lowercaseAddress = address.toLowerCase();
+        return this._web3.isAddress(lowercaseAddress);
+    }
+}
diff --git a/packages/testnet-faucets/src/ts/id_management.ts b/packages/testnet-faucets/src/ts/id_management.ts
new file mode 100644
index 000000000..930821172
--- /dev/null
+++ b/packages/testnet-faucets/src/ts/id_management.ts
@@ -0,0 +1,23 @@
+import EthereumTx = require('ethereumjs-tx');
+
+import { configs } from './configs';
+import { utils } from './utils';
+
+type Callback = (err: Error | null, accounts: any) => void;
+
+export const idManagement = {
+    getAccounts(callback: Callback) {
+        utils.consoleLog(`configs.DISPENSER_ADDRESS: ${configs.DISPENSER_ADDRESS}`);
+        callback(null, [configs.DISPENSER_ADDRESS]);
+    },
+    approveTransaction(txData: object, callback: Callback) {
+        callback(null, true);
+    },
+    signTransaction(txData: object, callback: Callback) {
+        const tx = new EthereumTx(txData);
+        const privateKeyBuffer = new Buffer(configs.DISPENSER_PRIVATE_KEY as string, 'hex');
+        tx.sign(privateKeyBuffer);
+        const rawTx = `0x${tx.serialize().toString('hex')}`;
+        callback(null, rawTx);
+    },
+};
diff --git a/packages/testnet-faucets/src/ts/request_queue.ts b/packages/testnet-faucets/src/ts/request_queue.ts
new file mode 100644
index 000000000..2b42ca4bf
--- /dev/null
+++ b/packages/testnet-faucets/src/ts/request_queue.ts
@@ -0,0 +1,56 @@
+import * as _ from 'lodash';
+import * as timers from 'timers';
+
+// HACK: web3 leaks XMLHttpRequest into the global scope and causes requests to hang
+// because they are using the wrong XHR package.
+// Filed issue: https://github.com/ethereum/web3.js/issues/844
+// tslint:disable-next-line:ordered-imports
+import * as Web3 from 'web3';
+
+const MAX_QUEUE_SIZE = 500;
+const DEFAULT_QUEUE_INTERVAL_MS = 1000;
+
+export class RequestQueue {
+    protected queueIntervalMs: number;
+    protected queue: string[];
+    protected queueIntervalId: NodeJS.Timer;
+    protected web3: Web3;
+    constructor(web3: any) {
+        this.queueIntervalMs = DEFAULT_QUEUE_INTERVAL_MS;
+        this.queue = [];
+
+        this.web3 = web3;
+
+        this.start();
+    }
+    public add(recipientAddress: string): boolean {
+        if (this.isFull()) {
+            return false;
+        }
+        this.queue.push(recipientAddress);
+        return true;
+    }
+    public size(): number {
+        return this.queue.length;
+    }
+    public isFull(): boolean {
+        return this.size() >= MAX_QUEUE_SIZE;
+    }
+    protected start() {
+        this.queueIntervalId = timers.setInterval(() => {
+            const recipientAddress = this.queue.shift();
+            if (_.isUndefined(recipientAddress)) {
+                return;
+            }
+            // tslint:disable-next-line:no-floating-promises
+            this.processNextRequestFireAndForgetAsync(recipientAddress);
+        }, this.queueIntervalMs);
+    }
+    protected stop() {
+        clearInterval(this.queueIntervalId);
+    }
+    // tslint:disable-next-line:prefer-function-over-method
+    protected async processNextRequestFireAndForgetAsync(recipientAddress: string) {
+        throw new Error('Expected processNextRequestFireAndForgetAsync to be implemented by a superclass');
+    }
+}
diff --git a/packages/testnet-faucets/src/ts/server.ts b/packages/testnet-faucets/src/ts/server.ts
new file mode 100644
index 000000000..23642787d
--- /dev/null
+++ b/packages/testnet-faucets/src/ts/server.ts
@@ -0,0 +1,28 @@
+import * as bodyParser from 'body-parser';
+import * as express from 'express';
+
+import { errorReporter } from './error_reporter';
+import { Handler } from './handler';
+
+// Setup the errorReporter to catch uncaught exceptions and unhandled rejections
+errorReporter.setup();
+
+const app = express();
+app.use(bodyParser.json()); // for parsing application/json
+app.use((req, res, next) => {
+    res.header('Access-Control-Allow-Origin', '*');
+    res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
+    next();
+});
+
+const handler = new Handler();
+app.get('/ping', (req: express.Request, res: express.Response) => {
+    res.status(200).send('pong');
+});
+app.get('/ether/:recipient', handler.dispenseEther.bind(handler));
+app.get('/zrx/:recipient', handler.dispenseZRX.bind(handler));
+
+// Log to rollbar any errors unhandled by handlers
+app.use(errorReporter.errorHandler());
+const port = process.env.PORT || 3000;
+app.listen(port);
diff --git a/packages/testnet-faucets/src/ts/utils.ts b/packages/testnet-faucets/src/ts/utils.ts
new file mode 100644
index 000000000..893f82ca3
--- /dev/null
+++ b/packages/testnet-faucets/src/ts/utils.ts
@@ -0,0 +1,7 @@
+export const utils = {
+    consoleLog(message: string) {
+        /* tslint:disable */
+        console.log(message);
+        /* tslint:enable */
+    },
+};
diff --git a/packages/testnet-faucets/src/ts/zrx_request_queue.ts b/packages/testnet-faucets/src/ts/zrx_request_queue.ts
new file mode 100644
index 000000000..bbc06f1de
--- /dev/null
+++ b/packages/testnet-faucets/src/ts/zrx_request_queue.ts
@@ -0,0 +1,45 @@
+import { ZeroEx } from '0x.js';
+import { BigNumber } from '@0xproject/utils';
+import * as _ from 'lodash';
+
+import { configs } from './configs';
+import { errorReporter } from './error_reporter';
+import { RequestQueue } from './request_queue';
+import { utils } from './utils';
+
+// HACK: web3 leaks XMLHttpRequest into the global scope and causes requests to hang
+// because they are using the wrong XHR package.
+// Filed issue: https://github.com/ethereum/web3.js/issues/844
+// tslint:disable-next-line:ordered-imports
+import * as Web3 from 'web3';
+
+const DISPENSE_AMOUNT_ZRX = new BigNumber(0.1);
+const QUEUE_INTERVAL_MS = 5000;
+
+export class ZRXRequestQueue extends RequestQueue {
+    private _zeroEx: ZeroEx;
+    constructor(web3: Web3) {
+        super(web3);
+        this.queueIntervalMs = QUEUE_INTERVAL_MS;
+        const zeroExConfig = {
+            networkId: configs.KOVAN_NETWORK_ID,
+        };
+        this._zeroEx = new ZeroEx(web3.currentProvider, zeroExConfig);
+    }
+    protected async processNextRequestFireAndForgetAsync(recipientAddress: string) {
+        utils.consoleLog(`Processing ZRX ${recipientAddress}`);
+        const baseUnitAmount = ZeroEx.toBaseUnitAmount(DISPENSE_AMOUNT_ZRX, 18);
+        try {
+            await this._zeroEx.token.transferAsync(
+                configs.ZRX_TOKEN_ADDRESS,
+                configs.DISPENSER_ADDRESS,
+                recipientAddress,
+                baseUnitAmount,
+            );
+            utils.consoleLog(`Sent ${DISPENSE_AMOUNT_ZRX} ZRX to ${recipientAddress}`);
+        } catch (err) {
+            utils.consoleLog(`Unexpected err: ${err} - ${JSON.stringify(err)}`);
+            await errorReporter.reportAsync(err);
+        }
+    }
+}
diff --git a/packages/testnet-faucets/tsconfig.json b/packages/testnet-faucets/tsconfig.json
new file mode 100644
index 000000000..7f0c084ff
--- /dev/null
+++ b/packages/testnet-faucets/tsconfig.json
@@ -0,0 +1,7 @@
+{
+    "extends": "../../tsconfig",
+    "compilerOptions": {
+        "outDir": "lib"
+    },
+    "include": ["./src/ts/**/*", "../../node_modules/web3-typescript-typings/index.d.ts"]
+}
diff --git a/packages/testnet-faucets/tslint.json b/packages/testnet-faucets/tslint.json
new file mode 100644
index 000000000..ffaefe83a
--- /dev/null
+++ b/packages/testnet-faucets/tslint.json
@@ -0,0 +1,3 @@
+{
+    "extends": ["@0xproject/tslint-config"]
+}
-- 
cgit v1.2.3