aboutsummaryrefslogtreecommitdiffstats
path: root/packages/testnet-faucets
diff options
context:
space:
mode:
Diffstat (limited to 'packages/testnet-faucets')
-rw-r--r--packages/testnet-faucets/Dockerfile13
-rw-r--r--packages/testnet-faucets/README.md129
-rw-r--r--packages/testnet-faucets/gulpfile.js92
-rw-r--r--packages/testnet-faucets/package.json43
-rw-r--r--packages/testnet-faucets/scripts/postpublish.js15
-rw-r--r--packages/testnet-faucets/src/ts/configs.ts7
-rw-r--r--packages/testnet-faucets/src/ts/error_reporter.ts40
-rw-r--r--packages/testnet-faucets/src/ts/ether_request_queue.ts27
-rw-r--r--packages/testnet-faucets/src/ts/global.d.ts26
-rw-r--r--packages/testnet-faucets/src/ts/handler.ts114
-rw-r--r--packages/testnet-faucets/src/ts/id_management.ts21
-rw-r--r--packages/testnet-faucets/src/ts/request_queue.ts56
-rw-r--r--packages/testnet-faucets/src/ts/rpc_urls.ts13
-rw-r--r--packages/testnet-faucets/src/ts/server.ts29
-rw-r--r--packages/testnet-faucets/src/ts/utils.ts7
-rw-r--r--packages/testnet-faucets/src/ts/zrx_request_queue.ts46
-rw-r--r--packages/testnet-faucets/tsconfig.json7
-rw-r--r--packages/testnet-faucets/tslint.json3
18 files changed, 688 insertions, 0 deletions
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..00352cc2a
--- /dev/null
+++ b/packages/testnet-faucets/README.md
@@ -0,0 +1,129 @@
+## @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 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}
+```
+
+If you want to talk to testrpc, set the following environment variable:
+
+```bash
+export FAUCET_ENVIRONMENT=development
+```
+
+Infura API Key can be requested here: https://infura.io/signup
+
+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 /ping`
+
+Returns `pong`
+
+`GET /info`
+
+Returns a JSON payload describing the state of the queues for each network. For example:
+
+```json
+{
+ "3":{
+ "ether":{
+ "full":false,
+ "size":4
+ },
+ "zrx":{
+ "full":false,
+ "size":6
+ }
+ },
+ "42":{
+ "ether":{
+ "full":false,
+ "size":8
+ },
+ "zrx":{
+ "full":false,
+ "size":20
+ }
+ }
+}
+```
+
+`GET /ether/:recipient`
+
+Where recipient is a hex encoded Ethereum address prefixed with `0x`.
+
+`GET /zrx/:recipient`
+
+Where recipient is a hex encoded Ethereum address prefixed with `0x`.
+
+#### Parameters
+
+The endpoints `/ether` and `/zrx` take a query parameter named `networkId` to specify the desired network where you would like to receive the ETH or ZRX. For example:
+
+```bash
+curl -i http://localhost:3000/ether/0x14e2F1F157E7DD4057D02817436D628A37120FD1\?networkId=3
+```
+
+This command will request the local server to initiate a transfer of 0.1 ETH from the dispensing address to `0x14e2F1F157E7DD4057D02817436D628A37120FD1` on the Ropsten testnet.
+
+If no `networkId` is provided via query parameters the faucet will default to network 42 (Kovan)
+
+### 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 \
+-e INFURA_API_KEY=$INFURA_API_KEY \
+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..bb0484a04
--- /dev/null
+++ b/packages/testnet-faucets/package.json
@@ -0,0 +1,43 @@
+{
+ "private": true,
+ "name": "@0xproject/testnet-faucets",
+ "version": "1.0.6",
+ "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.1",
+ "@0xproject/utils": "^0.2.1",
+ "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.3",
+ "@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.5",
+ "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..038c8e22a
--- /dev/null
+++ b/packages/testnet-faucets/src/ts/configs.ts
@@ -0,0 +1,7 @@
+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,
+ INFURA_API_KEY: process.env.INFURA_API_KEY,
+ ROLLBAR_ACCESS_KEY: process.env.FAUCET_ROLLBAR_ACCESS_KEY,
+};
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..bf5b3e81e
--- /dev/null
+++ b/packages/testnet-faucets/src/ts/handler.ts
@@ -0,0 +1,114 @@
+import { addressUtils } from '@0xproject/utils';
+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 { EtherRequestQueue } from './ether_request_queue';
+import { idManagement } from './id_management';
+import { RequestQueue } from './request_queue';
+import { rpcUrls } from './rpc_urls';
+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';
+
+interface RequestQueueByNetworkId {
+ [networkId: string]: RequestQueue;
+}
+
+enum QueueType {
+ ETH = 'ETH',
+ ZRX = 'ZRX',
+}
+
+const DEFAULT_NETWORK_ID = 42; // kovan
+
+export class Handler {
+ private _etherRequestQueueByNetworkId: RequestQueueByNetworkId = {};
+ private _zrxRequestQueueByNetworkId: RequestQueueByNetworkId = {};
+ constructor() {
+ _.forIn(rpcUrls, (rpcUrl: string, networkId: string) => {
+ const providerObj = this._createProviderEngine(rpcUrl);
+ const web3 = new Web3(providerObj);
+ this._etherRequestQueueByNetworkId[networkId] = new EtherRequestQueue(web3);
+ this._zrxRequestQueueByNetworkId[networkId] = new ZRXRequestQueue(web3, +networkId);
+ });
+ }
+ public getQueueInfo(req: express.Request, res: express.Response) {
+ res.setHeader('Content-Type', 'application/json');
+ const queueInfo = _.mapValues(rpcUrls, (rpcUrl: string, networkId: string) => {
+ utils.consoleLog(networkId);
+ const etherRequestQueue = this._etherRequestQueueByNetworkId[networkId];
+ const zrxRequestQueue = this._zrxRequestQueueByNetworkId[networkId];
+ return {
+ ether: {
+ full: etherRequestQueue.isFull(),
+ size: etherRequestQueue.size(),
+ },
+ zrx: {
+ full: zrxRequestQueue.isFull(),
+ size: zrxRequestQueue.size(),
+ },
+ };
+ });
+ const payload = JSON.stringify(queueInfo);
+ res.status(200).send(payload);
+ }
+ public dispenseEther(req: express.Request, res: express.Response) {
+ this._dispense(req, res, this._etherRequestQueueByNetworkId, QueueType.ETH);
+ }
+ public dispenseZRX(req: express.Request, res: express.Response) {
+ this._dispense(req, res, this._zrxRequestQueueByNetworkId, QueueType.ZRX);
+ }
+ private _dispense(
+ req: express.Request,
+ res: express.Response,
+ requestQueueByNetworkId: RequestQueueByNetworkId,
+ queueType: QueueType,
+ ) {
+ const recipientAddress = req.params.recipient;
+ if (_.isUndefined(recipientAddress) || !this._isValidEthereumAddress(recipientAddress)) {
+ res.status(400).send('INVALID_RECIPIENT_ADDRESS');
+ return;
+ }
+ const networkId = _.get(req.query, 'networkId', DEFAULT_NETWORK_ID);
+ const requestQueue = _.get(requestQueueByNetworkId, networkId);
+ if (_.isUndefined(requestQueue)) {
+ res.status(400).send('INVALID_NETWORK_ID');
+ return;
+ }
+ const lowerCaseRecipientAddress = recipientAddress.toLowerCase();
+ const didAddToQueue = requestQueue.add(lowerCaseRecipientAddress);
+ if (!didAddToQueue) {
+ res.status(503).send('QUEUE_IS_FULL');
+ return;
+ }
+ utils.consoleLog(`Added ${lowerCaseRecipientAddress} to queue: ${queueType} networkId: ${networkId}`);
+ res.status(200).end();
+ }
+ // 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;
+ }
+ // tslint:disable-next-line:prefer-function-over-method
+ private _isValidEthereumAddress(address: string): boolean {
+ const lowercaseAddress = address.toLowerCase();
+ return addressUtils.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..db9b610a3
--- /dev/null
+++ b/packages/testnet-faucets/src/ts/id_management.ts
@@ -0,0 +1,21 @@
+import EthereumTx = require('ethereumjs-tx');
+
+import { configs } from './configs';
+
+type Callback = (err: Error | null, accounts: any) => void;
+
+export const idManagement = {
+ getAccounts(callback: Callback) {
+ 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..20f2833a1
--- /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 subclass');
+ }
+}
diff --git a/packages/testnet-faucets/src/ts/rpc_urls.ts b/packages/testnet-faucets/src/ts/rpc_urls.ts
new file mode 100644
index 000000000..25a3b938f
--- /dev/null
+++ b/packages/testnet-faucets/src/ts/rpc_urls.ts
@@ -0,0 +1,13 @@
+import { configs } from './configs';
+
+const productionRpcUrls = {
+ '2': `https://ropsten.infura.io/${configs.INFURA_API_KEY}`,
+ '3': `https://rinkeby.infura.io/${configs.INFURA_API_KEY}`,
+ '42': `https://kovan.infura.io/${configs.INFURA_API_KEY}`,
+};
+
+const developmentRpcUrls = {
+ '50': 'http://127.0.0.1:8545',
+};
+
+export const rpcUrls = configs.ENVIRONMENT === 'development' ? developmentRpcUrls : productionRpcUrls;
diff --git a/packages/testnet-faucets/src/ts/server.ts b/packages/testnet-faucets/src/ts/server.ts
new file mode 100644
index 000000000..26edfff5a
--- /dev/null
+++ b/packages/testnet-faucets/src/ts/server.ts
@@ -0,0 +1,29 @@
+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('/info', handler.getQueueInfo.bind(handler));
+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..3d73f9dd2
--- /dev/null
+++ b/packages/testnet-faucets/src/ts/zrx_request_queue.ts
@@ -0,0 +1,46 @@
+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, networkId: number) {
+ super(web3);
+ this.queueIntervalMs = QUEUE_INTERVAL_MS;
+ const zeroExConfig = {
+ networkId,
+ };
+ 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 {
+ const zrxTokenAddress = this._zeroEx.exchange.getZRXTokenAddress();
+ const txHash = await this._zeroEx.token.transferAsync(
+ zrxTokenAddress,
+ configs.DISPENSER_ADDRESS,
+ recipientAddress,
+ baseUnitAmount,
+ );
+ utils.consoleLog(`Sent ${DISPENSE_AMOUNT_ZRX} ZRX to ${recipientAddress} tx: ${txHash}`);
+ } 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"]
+}