Skip to content

Commit

Permalink
ETH support (#338)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukechilds authored and sindresorhus committed Jun 13, 2018
1 parent e6b435b commit 2761bc7
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 43 deletions.
6 changes: 6 additions & 0 deletions app/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,9 @@ exports.ignoreExternalPrice = new Set([
'BEER',
'EQL',
]);

exports.hiddenCurrencies = [
'ETOMIC',
];

exports.appTimeStarted = Date.now();
21 changes: 21 additions & 0 deletions app/marketmaker/supported-currencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,27 @@ const supportedCurrencies = [
},
],
},
{
coin: 'ETH',
etomic: '0x0000000000000000000000000000000000000000',
rpcport: 80,
},
{
coin: 'ETOMIC',
name: 'Etomic',
asset: 'ETOMIC',
rpcport: 10271,
electrumServers: [
{
host: 'electrum1.cipig.net',
port: 10025,
},
{
host: 'electrum2.cipig.net',
port: 10025,
},
],
},
{
coin: 'FAIR',
rpcport: 40405,
Expand Down
80 changes: 70 additions & 10 deletions app/renderer/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const uuidPredicate = ow.string.matches(/^[a-z\d]+$/);
const errorWithObject = (message, object) => new Error(`${message}:\n${util.format(object)}`);
const genericError = object => errorWithObject('Encountered an error', object);

/* eslint-disable camelcase */
export default class Api {
constructor({endpoint, seedPhrase, concurrency = Infinity}) {
ow(endpoint, ow.string.label('endpoint'));
Expand Down Expand Up @@ -144,8 +145,8 @@ export default class Api {
rel,
});

const formatOrders = orders => orders
.filter(order => order.numutxos > 0)
const formatOrders = (orders, symbol) => orders
.filter(order => getCurrency(symbol).etomic ? true : (order.numutxos > 0))
.map(order => ({
address: order.address,
depth: order.depth,
Expand All @@ -159,8 +160,8 @@ export default class Api {
const formattedResponse = {
baseCurrency: response.base,
quoteCurrency: response.rel,
bids: formatOrders(response.bids),
asks: formatOrders(response.asks),
asks: formatOrders(response.asks, response.base),
bids: formatOrders(response.bids, response.rel),
};

return formattedResponse;
Expand Down Expand Up @@ -222,7 +223,7 @@ export default class Api {
return response.txfee;
}

async createTransaction(opts) {
async _createTransaction(opts) {
ow(opts.currency, symbolPredicate.label('currency')); // TODO: `currency` should be renamed to `symbol` for consistency
ow(opts.address, ow.string.label('address'));
ow(opts.amount, ow.number.positive.finite.label('amount'));
Expand All @@ -244,7 +245,7 @@ export default class Api {
};
}

async broadcastTransaction(currencySymbol, rawTransaction) {
async _broadcastTransaction(currencySymbol, rawTransaction) {
ow(currencySymbol, symbolPredicate.label('currencySymbol'));
ow(rawTransaction, ow.string.label('rawTransaction'));

Expand All @@ -261,23 +262,26 @@ export default class Api {
return response.txid;
}

async withdraw(opts) {
async _withdrawBtcFork(opts) {
ow(opts.currency, symbolPredicate.label('currency')); // TODO: `currency` should be renamed to `symbol` for consistency
ow(opts.address, ow.string.label('address'));
ow(opts.amount, ow.number.positive.finite.label('amount'));

const {
hex: rawTransaction,
txfee: txFeeSatoshis,
txid,
amount,
currency,
address,
} = await this.createTransaction(opts);
} = await this._createTransaction(opts);

// Convert from satoshis
const SATOSHIS = 100000000;
const txFee = txFeeSatoshis / SATOSHIS;

const broadcast = async () => {
// This is needed until a bug in marketmaker is resolved
await this.broadcastTransaction(opts.currency, rawTransaction);
await this._broadcastTransaction(opts.currency, rawTransaction);

return {txid, amount, currency, address};
};
Expand All @@ -288,6 +292,62 @@ export default class Api {
};
}

async _withdrawEth(opts) {
ow(opts.currency, symbolPredicate.label('currency')); // TODO: `currency` should be renamed to `symbol` for consistency
ow(opts.address, ow.string.label('address'));
ow(opts.amount, ow.number.positive.finite.label('amount'));

const {
eth_fee: txFee,
gas_price: gasPrice,
gas,
} = await this.request({
method: 'eth_withdraw',
coin: opts.currency,
to: opts.address,
amount: opts.amount,
broadcast: 0,
});

let hasBroadcast = false;
const broadcast = async () => {
if (hasBroadcast) {
throw new Error('Transaction has already been broadcast');
}
hasBroadcast = true;

const {tx_id} = await this.request({
method: 'eth_withdraw',
gas,
gas_price: gasPrice,
coin: opts.currency,
to: opts.address,
amount: opts.amount,
broadcast: 1,
});

return {
txid: tx_id,
currency: opts.currency,
amount: opts.amount,
address: opts.address,
};
};

return {
txFee,
broadcast,
};
}

withdraw(opts) {
ow(opts.currency, symbolPredicate.label('currency')); // TODO: `currency` should be renamed to `symbol` for consistency
ow(opts.address, ow.string.label('address'));
ow(opts.amount, ow.number.positive.finite.label('amount'));

return getCurrency(opts.currency).etomic ? this._withdrawEth(opts) : this._withdrawBtcFork(opts);
}

listUnspent(coin, address) {
ow(coin, symbolPredicate.label('coin'));
ow(address, ow.string.label('address'));
Expand Down
56 changes: 29 additions & 27 deletions app/renderer/containers/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Cycled from 'cycled';
import coinlist from 'coinlist';
import roundTo from 'round-to';
import {Container} from 'unstated';
import {appViews, alwaysEnabledCurrencies, ignoreExternalPrice} from '../../constants';
import {appViews, alwaysEnabledCurrencies, ignoreExternalPrice, hiddenCurrencies} from '../../constants';
import {getCurrencySymbols, getCurrencyName} from '../../marketmaker/supported-currencies';
import fireEvery from '../fire-every';
import {formatCurrency, setLoginWindowBounds} from '../util';
Expand Down Expand Up @@ -110,36 +110,38 @@ class AppContainer extends Container {
// TODO(sindresorhus): Move the returned `mm` currency info to a sub-property and only have cleaned-up top-level properties. For example, `mm` has too many properties for just the balance.

// Mixin useful data for the currencies
currencies = currencies.map(currency => {
currency.symbol = currency.coin; // For readability

const {cmcPriceUsd, cmcPercentChange24h} = this.getCurrencyPrice(currency.symbol);

if (cmcPriceUsd) {
currency.cmcPriceUsd = cmcPriceUsd;
currency.cmcBalanceUsd = currency.balance * cmcPriceUsd;
} else {
// We handle coins not on CMC
// `currency.price` is the price of the coin in KMD
currency.cmcPriceUsd = currency.price * kmdPriceInUsd;
currency.cmcBalanceUsd = currency.balance * currency.cmcPriceUsd;

// Don't show price for test currencies
if (excludedTestCurrencies.has(currency.symbol)) {
currency.cmcPriceUsd = 0;
currency.cmcBalanceUsd = 0;
currencies = currencies
.filter(currency => !hiddenCurrencies.includes(currency.coin))
.map(currency => {
currency.symbol = currency.coin; // For readability

const {cmcPriceUsd, cmcPercentChange24h} = this.getCurrencyPrice(currency.symbol);

if (cmcPriceUsd) {
currency.cmcPriceUsd = cmcPriceUsd;
currency.cmcBalanceUsd = currency.balance * cmcPriceUsd;
} else {
// We handle coins not on CMC
// `currency.price` is the price of the coin in KMD
currency.cmcPriceUsd = currency.price * kmdPriceInUsd;
currency.cmcBalanceUsd = currency.balance * currency.cmcPriceUsd;

// Don't show price for test currencies
if (excludedTestCurrencies.has(currency.symbol)) {
currency.cmcPriceUsd = 0;
currency.cmcBalanceUsd = 0;
}
}
}

currency.name = getCurrencyName(currency.symbol);
currency.cmcPercentChange24h = cmcPercentChange24h;
currency.name = getCurrencyName(currency.symbol);
currency.cmcPercentChange24h = cmcPercentChange24h;

currency.balanceFormatted = roundTo(currency.balance, 8);
currency.cmcPriceUsdFormatted = formatCurrency(currency.cmcPriceUsd);
currency.cmcBalanceUsdFormatted = formatCurrency(currency.cmcBalanceUsd);
currency.balanceFormatted = roundTo(currency.balance, 8);
currency.cmcPriceUsdFormatted = formatCurrency(currency.cmcPriceUsd);
currency.cmcBalanceUsdFormatted = formatCurrency(currency.cmcBalanceUsd);

return currency;
});
return currency;
});

if (!_.isEqual(this.state.currencies, currencies)) {
this.setState({currencies});
Expand Down
2 changes: 2 additions & 0 deletions app/renderer/containers/Login.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ class LoginContainer extends Container {
window._swapDB = swapDB;
}

// ETOMIC needs to be enabled first otherwise ETH/ERC20 tokens will fail
await api.enableCurrency('ETOMIC');
await Promise.all(appContainer.state.enabledCoins.map(x => api.enableCurrency(x)));

await appContainer.watchCMC();
Expand Down
1 change: 1 addition & 0 deletions app/renderer/views/Dashboard/DepositModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class DepositModal extends React.Component {
title={`Your ${currencyInfo.name} (${currencyInfo.symbol}) Wallet Address`}
open={this.state.isOpen}
onClose={this.close}
width="445px"
>
<React.Fragment>
<div className="section qrcode">
Expand Down
19 changes: 15 additions & 4 deletions app/renderer/views/Exchange/Order.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Select from 'components/Select';
import CurrencySelectOption from 'components/CurrencySelectOption';
import exchangeContainer from 'containers/Exchange';
import appContainer from 'containers/App';
import {getCurrency} from '../../../marketmaker/supported-currencies';
import {formatCurrency} from '../../util';
import './Order.scss';

Expand Down Expand Up @@ -48,6 +49,8 @@ class Top extends React.Component {
const Center = props => {
const {state} = exchangeContainer;

const isEtomic = getCurrency(props.type === 'buy' ? state.baseCurrency : state.quoteCurrency).etomic;

// TODO: This should be fixed properly in mm or use more sensible logic here
// This is just a quick fix to increase match rate for a demo
const selectRow = row => props.handlePriceChange(row.price * 1.05);
Expand All @@ -60,8 +63,12 @@ const Center = props => {
<thead>
<tr>
<th>Price ({state.quoteCurrency})</th>
<th>Avg Vol</th>
<th>Max Vol</th>
{!isEtomic && (
<React.Fragment>
<th>Avg Vol</th>
<th>Max Vol</th>
</React.Fragment>
)}
</tr>
</thead>
<tbody>
Expand All @@ -70,8 +77,12 @@ const Center = props => {
return props.getOrderBook().map((row, i) => (
<tr key={i} onClick={() => selectRow(row)}>
<td>{row.price}</td>
<td>{roundTo(row.averageVolume, 8)}</td>
<td>{roundTo(row.maxVolume, 8)}</td>
{!isEtomic && (
<React.Fragment>
<td>{roundTo(row.averageVolume, 8)}</td>
<td>{roundTo(row.maxVolume, 8)}</td>
</React.Fragment>
)}
</tr>
));
})()}
Expand Down
4 changes: 2 additions & 2 deletions app/renderer/views/Exchange/Order.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

.address {
font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 11px;
font-size: 9.5px;
}
}

Expand Down Expand Up @@ -137,7 +137,7 @@

@media (min-width: 1400px) {
.top .address {
font-size: 12px;
font-size: 11px;
}

.center .table-wrapper {
Expand Down

0 comments on commit 2761bc7

Please sign in to comment.