Grey out disabled swap options (#2026)

* Adjust emailTo to desired email addresses

* Prettierize SupportFooter

* Add monero swap support

* Adjust styling and wording for the PaymentInfo screen for XMR

* Only show warning on XMR swaps (duh)

* Replicate comment

* Fix styling for rates and payment info

* Add a Monero donation address and use it as the placeholder for the ReceivingAddress Input

* Add a public method for the ShapeShift service to add unavailable coins to coin list

* Show unavailable swap options in a sorted manner.

* Test implementation
This commit is contained in:
Connor Bryan 2018-07-06 13:56:54 -05:00 committed by Daniel Ternyak
parent 3ae3622ff4
commit f3bcc99603
4 changed files with 250 additions and 20 deletions

View File

@ -0,0 +1,125 @@
import shapeshift, { SHAPESHIFT_BASE_URL } from './shapeshift';
describe('ShapeShift service', () => {
beforeEach(() => {
(global as any).fetch = jest.fn().mockImplementation(
(url: string) =>
new Promise(resolve => {
const returnValues = {
[`${SHAPESHIFT_BASE_URL}/marketinfo`]: {
status: 200,
json: () => [
{
limit: 1,
maxLimit: 2,
min: 1,
minerFee: 2,
pair: 'BTC_ETH',
rate: '1.0'
},
{
limit: 1,
maxLimit: 2,
min: 1,
minerFee: 2,
pair: 'ETH_BTC',
rate: '1.0'
}
]
},
[`${SHAPESHIFT_BASE_URL}/getcoins`]: {
status: 200,
json: () => ({
BTC: {
name: 'Bitcoin',
symbol: 'BTC',
image: '',
imageSmall: '',
status: 'available',
minerFee: 1
},
ETH: {
name: 'Ethereum',
symbol: 'ETH',
image: '',
imageSmall: '',
status: 'available',
minerFee: 1
},
XMR: {
name: 'Monero',
symbol: 'XMR',
image: '',
imageSmall: '',
status: 'unavailable',
minerFee: 1
}
})
}
};
resolve(returnValues[url]);
})
);
});
it('provides a collection of all available and unavailable coins and tokens', async done => {
const rates = await shapeshift.getAllRates();
expect(rates).toEqual({
BTCETH: {
id: 'BTCETH',
rate: '1.0',
limit: 1,
min: 1,
options: [
{
id: 'BTC',
image: '',
name: 'Bitcoin',
status: 'available'
},
{
id: 'ETH',
image: '',
name: 'Ethereum',
status: 'available'
}
]
},
ETHBTC: {
id: 'ETHBTC',
rate: '1.0',
limit: 1,
min: 1,
options: [
{
id: 'ETH',
image: '',
name: 'Ethereum',
status: 'available'
},
{
id: 'BTC',
image: '',
name: 'Bitcoin',
status: 'available'
}
]
},
__XMR: {
id: '__XMR',
limit: 0,
min: 0,
options: [
{
id: 'XMR',
image: '',
name: 'Monero',
status: 'unavailable'
}
]
}
});
done();
});
});

View File

@ -1,9 +1,12 @@
import flatten from 'lodash/flatten';
import uniqBy from 'lodash/uniqBy';
import { checkHttpStatus, parseJSON } from 'api/utils';
const SHAPESHIFT_API_KEY =
export const SHAPESHIFT_API_KEY =
'8abde0f70ca69d5851702d57b10305705d7333e93263124cc2a2649dab7ff9cf86401fc8de7677e8edcd0e7f1eed5270b1b49be8806937ef95d64839e319e6d9';
const SHAPESHIFT_BASE_URL = 'https://shapeshift.io';
export const SHAPESHIFT_BASE_URL = 'https://shapeshift.io';
export const SHAPESHIFT_TOKEN_WHITELIST = [
'OMG',
@ -66,6 +69,29 @@ interface TokenMap {
};
}
interface ShapeshiftCoinInfo {
image: string;
imageSmall: string;
minerFee: number;
name: string;
status: string;
symbol: string;
}
interface ShapeshiftCoinInfoMap {
[id: string]: ShapeshiftCoinInfo;
}
interface ShapeshiftOption {
id?: string;
status?: string;
image?: string;
}
interface ShapeshiftOptionMap {
[symbol: string]: ShapeshiftOption;
}
class ShapeshiftService {
public whitelist = SHAPESHIFT_WHITELIST;
private url = SHAPESHIFT_BASE_URL;
@ -73,6 +99,8 @@ class ShapeshiftService {
private postHeaders = {
'Content-Type': 'application/json'
};
private supportedCoinsAndTokens: ShapeshiftCoinInfoMap = {};
private fetchedSupportedCoinsAndTokens = false;
public checkStatus(address: string) {
return fetch(`${this.url}/txStat/${address}`)
@ -118,19 +146,76 @@ class ShapeshiftService {
public getAllRates = async () => {
const marketInfo = await this.getMarketInfo();
const pairRates = await this.filterPairs(marketInfo);
const pairRates = this.filterPairs(marketInfo);
const checkAvl = await this.checkAvl(pairRates);
const mappedRates = this.mapMarketInfo(checkAvl);
return mappedRates;
const allRates = this.addUnavailableCoinsAndTokens(mappedRates);
return allRates;
};
public addUnavailableCoinsAndTokens = (availableCoinsAndTokens: TokenMap) => {
if (this.fetchedSupportedCoinsAndTokens) {
/** @desc Create a hash for efficiently checking which tokens are currently available. */
const allOptions = flatten(
Object.values(availableCoinsAndTokens).map(({ options }) => options)
);
const availableOptions: ShapeshiftOptionMap = uniqBy(allOptions, 'id').reduce(
(prev: ShapeshiftOptionMap, next) => {
prev[next.id] = next;
return prev;
},
{}
);
const unavailableCoinsAndTokens = this.whitelist
.map(token => {
/** @desc ShapeShift claims support for the token and it is available. */
const availableCoinOrToken = availableOptions[token];
if (availableCoinOrToken) {
return null;
}
/** @desc ShapeShift claims support for the token, but it is unavailable. */
const supportedCoinOrToken = this.supportedCoinsAndTokens[token];
if (supportedCoinOrToken) {
const { symbol: id, image, name, status } = supportedCoinOrToken;
return {
/** @desc Preface the false id with '__' to differentiate from actual pairs. */
id: `__${id}`,
limit: 0,
min: 0,
options: [{ id, image, name, status }]
};
}
/** @desc We claim support for the coin or token, but ShapeShift doesn't. */
return null;
})
.reduce((prev: ShapeshiftOptionMap, next) => {
if (next) {
prev[next.id] = next;
return prev;
}
return prev;
}, {});
return { ...availableCoinsAndTokens, ...unavailableCoinsAndTokens };
}
return availableCoinsAndTokens;
};
private filterPairs(marketInfo: ShapeshiftMarketInfo[]) {
return marketInfo.filter(obj => {
const { pair } = obj;
const pairArr = pair.split('_');
return this.whitelist.includes(pairArr[0]) && this.whitelist.includes(pairArr[1])
? true
: false;
return this.whitelist.includes(pairArr[0]) && this.whitelist.includes(pairArr[1]);
});
}
@ -165,7 +250,13 @@ class ShapeshiftService {
private getAvlCoins() {
return fetch(`${this.url}/getcoins`)
.then(checkHttpStatus)
.then(parseJSON);
.then(parseJSON)
.then(supportedCoinsAndTokens => {
this.supportedCoinsAndTokens = supportedCoinsAndTokens;
this.fetchedSupportedCoinsAndTokens = true;
return supportedCoinsAndTokens;
});
}
private getMarketInfo() {

View File

@ -87,7 +87,7 @@ class SwapDropdown extends PureComponent<Props, State> {
key={opt.name}
option={opt}
isMain={false}
isDisabled={opt.name === disabledOption}
isDisabled={opt.name === disabledOption || opt.status === 'unavailable'}
onChange={this.handleChange}
/>
))}
@ -140,6 +140,23 @@ class SwapDropdown extends PureComponent<Props, State> {
(opt1, opt2) => (opt1.id.toLowerCase() > opt2.id.toLowerCase() ? 1 : -1)
);
// Sort unavailable options last
otherOptions = otherOptions.sort((opt1, opt2) => {
if (opt1.status === 'available' && opt2.status === 'unavailable') {
return -1;
}
if (opt1.status === 'available' && opt2.status === 'available') {
return 0;
}
if (opt1.status === 'unavailable' && opt2.status === 'available') {
return 1;
}
return 0;
});
this.setState({ mainOptions, otherOptions });
}
}

View File

@ -71,22 +71,19 @@ export function swapReducer(state: types.SwapState = INITIAL_STATE, action: type
isFetchingRates: false
};
case types.SwapActions.LOAD_SHAPESHIFT_RATES_SUCCEEDED:
const {
entities: { providerRates: normalizedProviderRates, options: normalizedOptions }
} = normalize(action.payload, [providerRate]);
return {
...state,
shapeshiftRates: {
byId: normalize(action.payload, [providerRate]).entities.providerRates,
allIds: allIds(normalize(action.payload, [providerRate]).entities.providerRates)
byId: normalizedProviderRates,
allIds: allIds(normalizedProviderRates)
},
options: {
byId: Object.assign(
{},
normalize(action.payload, [providerRate]).entities.options,
state.options.byId
),
allIds: [
...allIds(normalize(action.payload, [providerRate]).entities.options),
...state.options.allIds
]
byId: { ...normalizedOptions, ...state.options.byId },
allIds: [...allIds(normalizedOptions), ...state.options.allIds]
},
isFetchingRates: false
};