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:
parent
3ae3622ff4
commit
f3bcc99603
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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() {
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue