Merge pull request #229 from 0xProject/feature/addKovanFaucets

Add kovan faucet project into the mono repo
This commit is contained in:
Brandon Millman
2017-12-13 15:09:39 -08:00
committed by GitHub
21 changed files with 1402 additions and 75 deletions

3
.gitignore vendored
View File

@@ -68,3 +68,6 @@ generated_docs/
TODO.md
packages/website/public/bundle*
# generated binaries
bin/

View File

@@ -34,3 +34,4 @@ This repository contains all the 0x developer tools written in TypeScript. Our h
| [`@0xproject/types`](/packages/types) | [![npm](https://img.shields.io/npm/v/@0xproject/types.svg?maxAge=2592000)](https://www.npmjs.com/package/@0xproject/types) | Shared types |
| [`@0xproject/utils`](/packages/utils) | [![npm](https://img.shields.io/npm/v/@0xproject/utils.svg?maxAge=2592000)](https://www.npmjs.com/package/@0xproject/utils) | Shared utils |
| [`@0xproject/web3-wrapper`](/packages/web3-wrapper) | [![npm](https://img.shields.io/npm/v/@0xproject/web3-wrapper.svg?maxAge=2592000)](https://www.npmjs.com/package/@0xproject/web3-wrapper) | Web3 wrapper |
| [`@0xproject/kovan_faucets`](/packages/kovan-faucets) | | A faucet micro-service that dispenses test ERC20 tokens or Ether |

View File

@@ -54,7 +54,7 @@
"yargs": "^10.0.3"
},
"dependencies": {
"0x.js": "^0.27.1",
"0x.js": "~0.27.2",
"@0xproject/json-schemas": "^0.6.10",
"@0xproject/utils": "^0.1.0",
"@0xproject/web3-wrapper": "^0.1.0",

View File

@@ -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"]

View File

@@ -0,0 +1,66 @@
Test Ether Faucet
----------------------
This faucet dispenses 0.1 test ether to one recipient per second. It has a max queue size of 1000.
## Install
Install project dependencies:
```
npm install
```
## Start
Set the following environment variables:
```
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`.
```
npm run 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`.
```javascript
{
"full": false,
"size": 0
}
```
## 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
```

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,44 @@
{
"private": true,
"name": "@0xproject/kovan_faucets",
"version": "1.0.0",
"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": {
"@0xproject/utils": "^0.1.0",
"0x.js": "~0.27.2",
"bignumber.js": "~4.1.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.2.1",
"@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.7.2",
"webpack": "^3.1.0",
"webpack-node-externals": "^1.6.0"
}
}

View File

@@ -0,0 +1,11 @@
export const configs = {
DISPENSER_ADDRESS: process.env.DISPENSER_ADDRESS.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,
};

View File

@@ -0,0 +1,41 @@
import * as express from 'express';
import * as fs from 'fs';
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', (err: Error) => {
utils.consoleLog(`Uncaught exception ${err}. Stack: ${err.stack}`);
this.report(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);
},
};

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,91 @@
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.
// Issue: https://github.com/trufflesuite/truffle-contract/issues/14
// 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);
}
}

View File

@@ -0,0 +1,25 @@
import EthereumTx = require('ethereumjs-tx');
import {configs} from './configs';
import {utils} from './utils';
type Callback = (err: Error, 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, 'hex');
tx.sign(privateKeyBuffer);
const rawTx = `0x${tx.serialize().toString('hex')}`;
callback(null, rawTx);
},
};

View File

@@ -0,0 +1,57 @@
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.
// Issue: https://github.com/trufflesuite/truffle-contract/issues/14
// 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(() => {
if (this.queue.length === 0) {
return;
}
const recipientAddress = this.queue.shift();
// 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');
}
}

View File

@@ -0,0 +1,26 @@
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);

View File

@@ -0,0 +1,10 @@
import * as express from 'express';
import * as _ from 'lodash';
export const utils = {
consoleLog(message: string) {
/* tslint:disable */
console.log(message);
/* tslint:enable */
},
};

View File

@@ -0,0 +1,43 @@
import {ZeroEx} from '0x.js';
import {promisify} from '@0xproject/utils';
import BigNumber from 'bignumber.js';
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.
// Issue: https://github.com/trufflesuite/truffle-contract/issues/14
// 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);
}
}
}

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [ "es2015", "dom" ],
"outDir": "lib",
"sourceMap": true,
"declaration": true,
"noImplicitAny": true,
"experimentalDecorators": true,
},
"include": [
"../../node_modules/web3-typescript-typings/index.d.ts",
"./src/ts/**/*"
]
}

View File

@@ -0,0 +1,5 @@
{
"extends": [
"@0xproject/tslint-config"
]
}

View File

@@ -18,7 +18,7 @@
"author": "Fabio Berger",
"license": "Apache-2.0",
"dependencies": {
"0x.js": "0.27.1",
"0x.js": "~0.27.2",
"@0xproject/subproviders": "^0.1.0",
"accounting": "^0.4.1",
"basscss": "^8.0.3",

876
yarn.lock

File diff suppressed because it is too large Load Diff