Shapeshift Integration (#564)

* progress

* Normalize bity api response

* Filter api response

* Track swap information in component state

* Update dropdown onchange

* remove dead code

* Update Min Max Validation

* Update minmax err msg && fix onChangeOriginKind

* Add origin & destination to redux state

* Update types & Update tests

* Update types

* Update swap.spec.ts test

* Remove commented out code

* Remove hardcoded coin array

* Create types.ts for swap reducer

* Update swapinput type

* Update bityRates in localStorage & Replace all instances of ...Kind / ...Amount props

* Add shapeshift banner

* initial work for sagas

* Update Types

* Update swap reducer initial state

* Update Types & Store empty obj for bityRates / options

* Update more types

* added shapeshift file and rates comments

* action reducers and prop mapping to components

* add typings and swap icon

* more actions reducers and sagas

* debugging shapeshift service

* add Headers

* Fix content type

* add order reset saga and ui fixes

* remove console log and swap b/w Bity and Shapeshift

* working state for Shapeshift and Bity - tested with mainnet

* add icon component

* UI improvements and fix select bug

* fix timer bug

* add bity fallback options and toFixed floats

* tslint errors

* add arrow to dropdown and add support footer

* Add service provider

* fix minor $ bug and stop timer on order complete

* better load UX and dropdown UX

* fixed single test

* currRate prop bugs and reduce LS bloat

* takeEvery on timer saga and don't clear state.options to restartSwap reducer

* export tx sagas and fix minor type

* Add ShapeShift Rates functionality when selecting a ShapeShift pair.

* type fixes

* BugFix: Don't change displayed ShapeShift Rate Inputs on every dropdown change
Also contains some caching / performance improvements

* BugFix: Don't remote rate inputs when falsy amount

* fix type error

* Progress commit

* Implement saga logic

* Make address field factory component

* Shorten debounce time

* Make new actions / sagas  for handling single token lookup

* Implement working version of litesend

* Change saga into selector

* Add failing spec

* fix broken test

* add debounce to error message

* fix tests

* update snapshots

* test coverage

* move setState disabled property from debounce so we instantly can go to next step on valid amounts

* much deeper test coverage, fix debounce ux, and fix bity flashing at swap page load

* fix minor failing test

* seperate shapeshift erc20 token whitelist

* fix saveState store bug

* break orderTimeRemaining saga up and rewrite tests

* add new swap icon

* remove unused allowReadOnly prop

* change offlineaware to walletdecrypt for litesend

* fix LiteSend changewallet bug

* fix error message UX

* fix button styling to match develop

* fix liteSend test

* Fix LiteSend UX on unavl tokens, dropdown null value, and don't show decrypt in litesend after successful wallet decrypt.

* add litesend network check
This commit is contained in:
Eddie Wang 2018-01-02 12:04:50 -06:00 committed by Daniel Ternyak
parent 2f98555b00
commit 88532cdc3c
71 changed files with 3334 additions and 398 deletions

View File

@ -27,6 +27,16 @@ export function loadBityRatesSucceededSwap(
};
}
export type TLoadShapeshiftSucceededSwap = typeof loadShapeshiftRatesSucceededSwap;
export function loadShapeshiftRatesSucceededSwap(
payload
): interfaces.LoadShapshiftRatesSucceededSwapAction {
return {
type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED,
payload
};
}
export type TDestinationAddressSwap = typeof destinationAddressSwap;
export function destinationAddressSwap(payload?: string): interfaces.DestinationAddressSwapAction {
return {
@ -49,6 +59,13 @@ export function loadBityRatesRequestedSwap(): interfaces.LoadBityRatesRequestedS
};
}
export type TLoadShapeshiftRequestedSwap = typeof loadShapeshiftRatesRequestedSwap;
export function loadShapeshiftRatesRequestedSwap(): interfaces.LoadShapeshiftRequestedSwapAction {
return {
type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED
};
}
export type TStopLoadBityRatesSwap = typeof stopLoadBityRatesSwap;
export function stopLoadBityRatesSwap(): interfaces.StopLoadBityRatesSwapAction {
return {
@ -56,6 +73,13 @@ export function stopLoadBityRatesSwap(): interfaces.StopLoadBityRatesSwapAction
};
}
export type TStopLoadShapeshiftRatesSwap = typeof stopLoadShapeshiftRatesSwap;
export function stopLoadShapeshiftRatesSwap(): interfaces.StopLoadShapeshiftRatesSwapAction {
return {
type: TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES
};
}
export type TOrderTimeSwap = typeof orderTimeSwap;
export function orderTimeSwap(payload: number): interfaces.OrderSwapTimeSwapAction {
return {
@ -74,6 +98,16 @@ export function bityOrderCreateSucceededSwap(
};
}
export type TShapeshiftOrderCreateSucceededSwap = typeof shapeshiftOrderCreateSucceededSwap;
export function shapeshiftOrderCreateSucceededSwap(
payload: interfaces.ShapeshiftOrderResponse
): interfaces.ShapeshiftOrderCreateSucceededSwapAction {
return {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED,
payload
};
}
export type TBityOrderCreateRequestedSwap = typeof bityOrderCreateRequestedSwap;
export function bityOrderCreateRequestedSwap(
amount: number,
@ -82,7 +116,7 @@ export function bityOrderCreateRequestedSwap(
mode: number = 0
): interfaces.BityOrderCreateRequestedSwapAction {
return {
type: TypeKeys.SWAP_ORDER_CREATE_REQUESTED,
type: TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED,
payload: {
amount,
destinationAddress,
@ -92,29 +126,70 @@ export function bityOrderCreateRequestedSwap(
};
}
export function bityOrderCreateFailedSwap(): interfaces.BityOrderCreateFailedSwapAction {
export type TShapeshiftOrderCreateRequestedSwap = typeof shapeshiftOrderCreateRequestedSwap;
export function shapeshiftOrderCreateRequestedSwap(
withdrawal: string,
originKind: string,
destinationKind: string,
destinationAmount: number
): interfaces.ShapeshiftOrderCreateRequestedSwapAction {
return {
type: TypeKeys.SWAP_ORDER_CREATE_FAILED
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED,
payload: {
withdrawal,
originKind,
destinationKind,
destinationAmount
}
};
}
export type TOrderStatusSucceededSwap = typeof orderStatusSucceededSwap;
export function orderStatusSucceededSwap(
export function bityOrderCreateFailedSwap(): interfaces.BityOrderCreateFailedSwapAction {
return {
type: TypeKeys.SWAP_BITY_ORDER_CREATE_FAILED
};
}
export function shapeshiftOrderCreateFailedSwap(): interfaces.ShapeshiftOrderCreateFailedSwapAction {
return {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_FAILED
};
}
export type TBityOrderStatusSucceededSwap = typeof bityOrderStatusSucceededSwap;
export function bityOrderStatusSucceededSwap(
payload: interfaces.BityOrderResponse
): interfaces.OrderStatusSucceededSwapAction {
): interfaces.BityOrderStatusSucceededSwapAction {
return {
type: TypeKeys.SWAP_BITY_ORDER_STATUS_SUCCEEDED,
payload
};
}
export type TOrderStatusRequestedSwap = typeof orderStatusRequestedSwap;
export function orderStatusRequestedSwap(): interfaces.OrderStatusRequestedSwapAction {
export type TShapeshiftOrderStatusSucceededSwap = typeof shapeshiftOrderStatusSucceededSwap;
export function shapeshiftOrderStatusSucceededSwap(
payload: interfaces.ShapeshiftStatusResponse
): interfaces.ShapeshiftOrderStatusSucceededSwapAction {
return {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED,
payload
};
}
export type TBityOrderStatusRequestedSwap = typeof bityOrderStatusRequested;
export function bityOrderStatusRequested(): interfaces.BityOrderStatusRequestedSwapAction {
return {
type: TypeKeys.SWAP_BITY_ORDER_STATUS_REQUESTED
};
}
export type TShapeshiftOrderStatusRequestedSwap = typeof shapeshiftOrderStatusRequested;
export function shapeshiftOrderStatusRequested(): interfaces.ShapeshiftOrderStatusRequestedSwapAction {
return {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_REQUESTED
};
}
export type TStartOrderTimerSwap = typeof startOrderTimerSwap;
export function startOrderTimerSwap(): interfaces.StartOrderTimerSwapAction {
return {
@ -136,9 +211,45 @@ export function startPollBityOrderStatus(): interfaces.StartPollBityOrderStatusA
};
}
export type TStartPollShapeshiftOrderStatus = typeof startPollShapeshiftOrderStatus;
export function startPollShapeshiftOrderStatus(): interfaces.StartPollShapeshiftOrderStatusAction {
return {
type: TypeKeys.SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS
};
}
export type TStopPollBityOrderStatus = typeof stopPollBityOrderStatus;
export function stopPollBityOrderStatus(): interfaces.StopPollBityOrderStatusAction {
return {
type: TypeKeys.SWAP_STOP_POLL_BITY_ORDER_STATUS
};
}
export type TStopPollShapeshiftOrderStatus = typeof stopPollShapeshiftOrderStatus;
export function stopPollShapeshiftOrderStatus(): interfaces.StopPollShapeshiftOrderStatusAction {
return {
type: TypeKeys.SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS
};
}
export type TConfigureLiteSend = typeof configureLiteSend;
export function configureLiteSend(): interfaces.ConfigureLiteSendAction {
return { type: TypeKeys.SWAP_CONFIGURE_LITE_SEND };
}
export type TShowLiteSend = typeof showLiteSend;
export function showLiteSend(
payload: interfaces.ShowLiteSendAction['payload']
): interfaces.ShowLiteSendAction {
return { type: TypeKeys.SWAP_SHOW_LITE_SEND, payload };
}
export type TChangeSwapProvider = typeof changeSwapProvider;
export function changeSwapProvider(
payload: interfaces.ProviderName
): interfaces.ChangeProviderSwapAcion {
return {
type: TypeKeys.SWAP_CHANGE_PROVIDER,
payload
};
}

View File

@ -9,7 +9,7 @@ export interface Pairs {
export interface SwapInput {
id: string;
amount: number;
amount: number | string;
}
export interface SwapInputs {
@ -24,6 +24,8 @@ export interface InitSwap {
export interface Option {
id: string;
status?: string;
image?: string;
}
export interface ApiResponseObj {
@ -41,6 +43,11 @@ export interface LoadBityRatesSucceededSwapAction {
payload: ApiResponse;
}
export interface LoadShapshiftRatesSucceededSwapAction {
type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED;
payload: ApiResponse;
}
export interface DestinationAddressSwapAction {
type: TypeKeys.SWAP_DESTINATION_ADDRESS;
payload?: string;
@ -55,6 +62,11 @@ export interface LoadBityRatesRequestedSwapAction {
payload?: null;
}
export interface LoadShapeshiftRequestedSwapAction {
type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED;
payload?: null;
}
export interface ChangeStepSwapAction {
type: TypeKeys.SWAP_STEP;
payload: number;
@ -64,13 +76,17 @@ export interface StopLoadBityRatesSwapAction {
type: TypeKeys.SWAP_STOP_LOAD_BITY_RATES;
}
export interface StopLoadShapeshiftRatesSwapAction {
type: TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES;
}
export interface OrderSwapTimeSwapAction {
type: TypeKeys.SWAP_ORDER_TIME;
payload: number;
}
export interface BityOrderCreateRequestedSwapAction {
type: TypeKeys.SWAP_ORDER_CREATE_REQUESTED;
type: TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED;
payload: {
amount: number;
destinationAddress: string;
@ -79,6 +95,16 @@ export interface BityOrderCreateRequestedSwapAction {
};
}
export interface ShapeshiftOrderCreateRequestedSwapAction {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED;
payload: {
withdrawal: string;
originKind: string;
destinationKind: string;
destinationAmount: number;
};
}
export interface BityOrderInput {
amount: string;
currency: string;
@ -99,6 +125,31 @@ export interface BityOrderResponse {
status: string;
}
export interface ShapeshiftOrderResponse {
apiPubKey?: string;
deposit: string;
depositAmount: string;
expiration: number;
expirationFormatted?: string;
inputCurrency?: string;
maxLimit: number;
minerFee: string;
orderId: string;
outputCurrency?: string;
pair: string; // e.g. eth_bat
provider?: ProviderName; // shapeshift
quotedRate: string;
withdrawal: string;
withdrawalAmount: string;
}
export interface ShapeshiftStatusResponse {
status: string;
address?: string;
withdraw?: string;
transaction: string;
}
export type BityOrderPostResponse = BityOrderResponse & {
payment_address: string;
status: string;
@ -109,23 +160,44 @@ export type BityOrderPostResponse = BityOrderResponse & {
id: string;
};
export type ProviderName = 'shapeshift' | 'bity';
export interface BityOrderCreateSucceededSwapAction {
type: TypeKeys.SWAP_BITY_ORDER_CREATE_SUCCEEDED;
payload: BityOrderPostResponse;
}
export interface BityOrderCreateFailedSwapAction {
type: TypeKeys.SWAP_ORDER_CREATE_FAILED;
export interface ShapeshiftOrderCreateSucceededSwapAction {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED;
payload: ShapeshiftOrderResponse;
}
export interface OrderStatusRequestedSwapAction {
export interface BityOrderCreateFailedSwapAction {
type: TypeKeys.SWAP_BITY_ORDER_CREATE_FAILED;
}
export interface ShapeshiftOrderCreateFailedSwapAction {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_FAILED;
}
export interface BityOrderStatusRequestedSwapAction {
type: TypeKeys.SWAP_BITY_ORDER_STATUS_REQUESTED;
}
export interface OrderStatusSucceededSwapAction {
export interface ShapeshiftOrderStatusRequestedSwapAction {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_REQUESTED;
}
export interface BityOrderStatusSucceededSwapAction {
type: TypeKeys.SWAP_BITY_ORDER_STATUS_SUCCEEDED;
payload: BityOrderResponse;
}
export interface ShapeshiftOrderStatusSucceededSwapAction {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED;
payload: ShapeshiftStatusResponse;
}
export interface StartOrderTimerSwapAction {
type: TypeKeys.SWAP_ORDER_START_TIMER;
}
@ -138,22 +210,55 @@ export interface StartPollBityOrderStatusAction {
type: TypeKeys.SWAP_START_POLL_BITY_ORDER_STATUS;
}
export interface StartPollShapeshiftOrderStatusAction {
type: TypeKeys.SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS;
}
export interface StopPollBityOrderStatusAction {
type: TypeKeys.SWAP_STOP_POLL_BITY_ORDER_STATUS;
}
export interface StopPollShapeshiftOrderStatusAction {
type: TypeKeys.SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS;
}
export interface ChangeProviderSwapAcion {
type: TypeKeys.SWAP_CHANGE_PROVIDER;
payload: ProviderName;
}
export interface ConfigureLiteSendAction {
type: TypeKeys.SWAP_CONFIGURE_LITE_SEND;
}
export interface ShowLiteSendAction {
type: TypeKeys.SWAP_SHOW_LITE_SEND;
payload: boolean;
}
/*** Action Type Union ***/
export type SwapAction =
| ChangeStepSwapAction
| InitSwap
| LoadBityRatesSucceededSwapAction
| LoadShapshiftRatesSucceededSwapAction
| DestinationAddressSwapAction
| RestartSwapAction
| LoadBityRatesRequestedSwapAction
| LoadShapeshiftRequestedSwapAction
| StopLoadBityRatesSwapAction
| StopLoadShapeshiftRatesSwapAction
| BityOrderCreateRequestedSwapAction
| ShapeshiftOrderCreateRequestedSwapAction
| BityOrderCreateSucceededSwapAction
| OrderStatusSucceededSwapAction
| ShapeshiftOrderCreateSucceededSwapAction
| BityOrderStatusSucceededSwapAction
| ShapeshiftOrderStatusSucceededSwapAction
| StartPollBityOrderStatusAction
| StartPollShapeshiftOrderStatusAction
| BityOrderCreateFailedSwapAction
| OrderSwapTimeSwapAction;
| ShapeshiftOrderCreateFailedSwapAction
| OrderSwapTimeSwapAction
| ChangeProviderSwapAcion
| ConfigureLiteSendAction
| ShowLiteSendAction;

View File

@ -2,18 +2,31 @@ export enum TypeKeys {
SWAP_STEP = 'SWAP_STEP',
SWAP_INIT = 'SWAP_INIT',
SWAP_LOAD_BITY_RATES_SUCCEEDED = 'SWAP_LOAD_BITY_RATES_SUCCEEDED',
SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED = 'SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED',
SWAP_DESTINATION_ADDRESS = 'SWAP_DESTINATION_ADDRESS',
SWAP_RESTART = 'SWAP_RESTART',
SWAP_LOAD_BITY_RATES_REQUESTED = 'SWAP_LOAD_BITY_RATES_REQUESTED',
SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED = 'SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED',
SWAP_STOP_LOAD_BITY_RATES = 'SWAP_STOP_LOAD_BITY_RATES',
SWAP_STOP_LOAD_SHAPESHIFT_RATES = 'SWAP_STOP_LOAD_SHAPESHIFT_RATES',
SWAP_ORDER_TIME = 'SWAP_ORDER_TIME',
SWAP_BITY_ORDER_CREATE_SUCCEEDED = 'SWAP_BITY_ORDER_CREATE_SUCCEEDED',
SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED = 'SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED',
SWAP_BITY_ORDER_STATUS_SUCCEEDED = 'SWAP_BITY_ORDER_STATUS_SUCCEEDED',
SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED = 'SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED',
SWAP_BITY_ORDER_STATUS_REQUESTED = 'SWAP_BITY_ORDER_STATUS_REQUESTED',
SWAP_SHAPESHIFT_ORDER_STATUS_REQUESTED = 'SWAP_SHAPESHIFT_ORDER_STATUS_REQUESTED',
SWAP_ORDER_START_TIMER = 'SWAP_ORDER_START_TIMER',
SWAP_ORDER_STOP_TIMER = 'SWAP_ORDER_STOP_TIMER',
SWAP_START_POLL_BITY_ORDER_STATUS = 'SWAP_START_POLL_BITY_ORDER_STATUS',
SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS = 'SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS',
SWAP_STOP_POLL_BITY_ORDER_STATUS = 'SWAP_STOP_POLL_BITY_ORDER_STATUS',
SWAP_ORDER_CREATE_REQUESTED = 'SWAP_ORDER_CREATE_REQUESTED',
SWAP_ORDER_CREATE_FAILED = 'SWAP_ORDER_CREATE_FAILED'
SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS = 'SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS',
SWAP_BITY_ORDER_CREATE_REQUESTED = 'SWAP_ORDER_CREATE_REQUESTED',
SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED = 'SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED',
SWAP_BITY_ORDER_CREATE_FAILED = 'SWAP_ORDER_CREATE_FAILED',
SWAP_SHAPESHIFT_ORDER_CREATE_FAILED = 'SWAP_SHAPESHIFT_ORDER_CREATE_FAILED',
SWAP_CHANGE_PROVIDER = 'SWAP_CHANGE_PROVIDER',
SWAP_CONFIGURE_LITE_SEND = 'SWAP_CONFIGURE_LITE_SEND',
SWAP_SHOW_LITE_SEND = 'SWAP_SHOW_LITE_SEND'
}

View File

@ -88,6 +88,34 @@ export function setTokenBalancesRejected(): types.SetTokenBalancesRejectedAction
};
}
export function setTokenBalancePending(
payload: types.SetTokenBalancePendingAction['payload']
): types.SetTokenBalancePendingAction {
return {
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_PENDING,
payload
};
}
export type TSetTokenBalanceFulfilled = typeof setTokenBalanceFulfilled;
export function setTokenBalanceFulfilled(payload: {
[key: string]: {
balance: TokenValue;
error: string | null;
};
}): types.SetTokenBalanceFulfilledAction {
return {
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_FULFILLED,
payload
};
}
export function setTokenBalanceRejected(): types.SetTokenBalanceRejectedAction {
return {
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_REJECTED
};
}
export type TScanWalletForTokens = typeof scanWalletForTokens;
export function scanWalletForTokens(wallet: IWallet): types.ScanWalletForTokensAction {
return {

View File

@ -63,6 +63,25 @@ export interface SetTokenBalancesRejectedAction {
type: TypeKeys.WALLET_SET_TOKEN_BALANCES_REJECTED;
}
export interface SetTokenBalancePendingAction {
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_PENDING;
payload: { tokenSymbol: string };
}
export interface SetTokenBalanceFulfilledAction {
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_FULFILLED;
payload: {
[key: string]: {
balance: TokenValue;
error: string | null;
};
};
}
export interface SetTokenBalanceRejectedAction {
type: TypeKeys.WALLET_SET_TOKEN_BALANCE_REJECTED;
}
export interface ScanWalletForTokensAction {
type: TypeKeys.WALLET_SCAN_WALLET_FOR_TOKENS;
payload: IWallet;
@ -108,6 +127,9 @@ export type WalletAction =
| SetTokenBalancesPendingAction
| SetTokenBalancesFulfilledAction
| SetTokenBalancesRejectedAction
| SetTokenBalancePendingAction
| SetTokenBalanceFulfilledAction
| SetTokenBalanceRejectedAction
| ScanWalletForTokensAction
| SetWalletTokensAction
| SetWalletConfigAction;

View File

@ -10,6 +10,9 @@ export enum TypeKeys {
WALLET_SET_TOKEN_BALANCES_PENDING = 'WALLET_SET_TOKEN_BALANCES_PENDING',
WALLET_SET_TOKEN_BALANCES_FULFILLED = 'WALLET_SET_TOKEN_BALANCES_FULFILLED',
WALLET_SET_TOKEN_BALANCES_REJECTED = 'WALLET_SET_TOKEN_BALANCES_REJECTED',
WALLET_SET_TOKEN_BALANCE_PENDING = 'WALLET_SET_TOKEN_BALANCE_PENDING',
WALLET_SET_TOKEN_BALANCE_FULFILLED = 'WALLET_SET_TOKEN_BALANCE_FULFILLED',
WALLET_SET_TOKEN_BALANCE_REJECTED = 'WALLET_SET_TOKEN_BALANCE_REJECTED',
WALLET_SCAN_WALLET_FOR_TOKENS = 'WALLET_SCAN_WALLET_FOR_TOKENS',
WALLET_SET_WALLET_TOKENS = 'WALLET_SET_WALLET_TOKENS',
WALLET_SET_CONFIG = 'WALLET_SET_CONFIG',

View File

@ -1,10 +1,34 @@
import bityConfig, { WhitelistedCoins } from 'config/bity';
import { checkHttpStatus, parseJSON, filter } from './utils';
import bitcoinIcon from 'assets/images/bitcoin.png';
import repIcon from 'assets/images/augur.png';
import etherIcon from 'assets/images/ether.png';
const isCryptoPair = (from: string, to: string, arr: WhitelistedCoins[]) => {
return filter(from, arr) && filter(to, arr);
};
const btcOptions = {
id: 'BTC',
status: 'available',
image: bitcoinIcon,
name: 'Bitcoin'
};
const ethOptions = {
id: 'ETH',
status: 'available',
image: etherIcon,
name: 'Ether'
};
const repOptions = {
id: 'REP',
status: 'available',
image: repIcon,
name: 'Augur'
};
export function getAllRates() {
const mappedRates = {};
return _getAllRates().then(bityRates => {
@ -14,9 +38,31 @@ export function getAllRates() {
const to = { id: pairName.substring(3, 6) };
// Check if rate exists= && check if the pair only crypto to crypto, not crypto to fiat, or any other combination
if (parseFloat(each.rate_we_sell) && isCryptoPair(from.id, to.id, ['BTC', 'ETH', 'REP'])) {
let fromOptions;
let toOptions;
switch (from.id) {
case 'BTC':
fromOptions = btcOptions;
break;
case 'ETH':
fromOptions = ethOptions;
break;
case 'REP':
fromOptions = repOptions;
}
switch (to.id) {
case 'BTC':
toOptions = btcOptions;
break;
case 'ETH':
toOptions = ethOptions;
break;
case 'REP':
toOptions = repOptions;
}
mappedRates[pairName] = {
id: pairName,
options: [from, to],
options: [fromOptions, toOptions],
rate: parseFloat(each.rate_we_sell)
};
}

175
common/api/shapeshift.ts Normal file
View File

@ -0,0 +1,175 @@
import { checkHttpStatus, parseJSON } from 'api/utils';
const SHAPESHIFT_BASE_URL = 'https://shapeshift.io';
export const SHAPESHIFT_TOKEN_WHITELIST = [
'OMG',
'REP',
'SNT',
'SNGLS',
'ZRX',
'SWT',
'ANT',
'BAT',
'BNT',
'CVC',
'DNT',
'1ST',
'GNO',
'GNT',
'EDG',
'FUN',
'RLC',
'TRST',
'GUP',
'ETH'
];
export const SHAPESHIFT_WHITELIST = [...SHAPESHIFT_TOKEN_WHITELIST, 'ETC', 'BTC'];
class ShapeshiftService {
public whitelist = SHAPESHIFT_WHITELIST;
private url = SHAPESHIFT_BASE_URL;
private apiKey = '0ca1ccd50b708a3f8c02327f0caeeece06d3ddc1b0ac749a987b453ee0f4a29bdb5da2e53bc35e57fb4bb7ae1f43c93bb098c3c4716375fc1001c55d8c94c160';
private postHeaders = {
'Content-Type': 'application/json'
};
public checkStatus(address) {
return fetch(`${this.url}/txStat/${address}`)
.then(checkHttpStatus)
.then(parseJSON);
}
public sendAmount(withdrawal, originKind, destinationKind, destinationAmount) {
const pair = `${originKind.toLowerCase()}_${destinationKind.toLowerCase()}`;
return fetch(`${this.url}/sendamount`, {
method: 'POST',
body: JSON.stringify({
amount: destinationAmount,
pair,
apiKey: this.apiKey,
withdrawal
}),
headers: new Headers(this.postHeaders)
})
.then(checkHttpStatus)
.then(parseJSON);
}
public getCoins() {
return fetch(`${this.url}/getcoins`)
.then(checkHttpStatus)
.then(parseJSON);
}
public getAllRates = async () => {
const marketInfo = await this.getMarketInfo();
const pairRates = await this.getPairRates(marketInfo);
const checkAvl = await this.checkAvl(pairRates);
const mappedRates = this.mapMarketInfo(checkAvl);
return mappedRates;
};
private getPairRates(marketInfo) {
const filteredMarketInfo = marketInfo.filter(obj => {
const { pair } = obj;
const pairArr = pair.split('_');
return this.whitelist.includes(pairArr[0]) && this.whitelist.includes(pairArr[1])
? true
: false;
});
const pairRates = filteredMarketInfo.map(p => {
const { pair } = p;
const singlePair = Promise.resolve(this.getSinglePairRate(pair));
return { ...p, ...singlePair };
});
return pairRates;
}
private async checkAvl(pairRates) {
const avlCoins = await this.getAvlCoins();
const mapAvl = pairRates.map(p => {
const { pair } = p;
const pairArr = pair.split('_');
if (pairArr[0] in avlCoins && pairArr[1] in avlCoins) {
return {
...p,
...{
[pairArr[0]]: {
name: avlCoins[pairArr[0]].name,
status: avlCoins[pairArr[0]].status,
image: avlCoins[pairArr[0]].image
},
[pairArr[1]]: {
name: avlCoins[pairArr[1]].name,
status: avlCoins[pairArr[1]].status,
image: avlCoins[pairArr[1]].image
}
}
};
}
});
return mapAvl;
}
private getAvlCoins() {
return fetch(`${this.url}/getcoins`)
.then(checkHttpStatus)
.then(parseJSON);
}
private getSinglePairRate(pair) {
return fetch(`${this.url}/rate/${pair}`)
.then(checkHttpStatus)
.then(parseJSON);
}
private getMarketInfo() {
return fetch(`${this.url}/marketinfo`)
.then(checkHttpStatus)
.then(parseJSON);
}
private isWhitelisted(coin) {
return this.whitelist.includes(coin);
}
private mapMarketInfo(marketInfo) {
const tokenMap = {};
marketInfo.forEach(m => {
const originKind = m.pair.substring(0, 3);
const destinationKind = m.pair.substring(4, 7);
if (this.isWhitelisted(originKind) && this.isWhitelisted(destinationKind)) {
const pairName = originKind + destinationKind;
const { rate, limit, min } = m;
tokenMap[pairName] = {
id: pairName,
options: [
{
id: originKind,
status: m[originKind].status,
image: m[originKind].image,
name: m[originKind].name
},
{
id: destinationKind,
status: m[destinationKind].status,
image: m[destinationKind].image,
name: m[destinationKind].name
}
],
rate,
limit,
min
};
}
});
return tokenMap;
}
}
const shapeshift = new ShapeshiftService();
export default shapeshift;

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1050 350" style="enable-background:new 0 0 1050 350;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#273C51;}
.st2{fill:url(#SVGID_1_);}
.st3{fill:#466284;}
.st4{fill:#354D6A;}
.st5{fill:url(#SVGID_2_);}
.st6{fill:url(#SVGID_3_);}
.st7{fill:url(#SVGID_4_);}
.st8{fill:url(#SVGID_5_);}
.st9{fill:url(#SVGID_6_);}
.st10{fill:url(#SVGID_7_);}
.st11{fill:url(#SVGID_8_);}
.st12{fill:url(#SVGID_9_);}
.st13{fill:url(#SVGID_10_);}
.st14{fill:none;}
</style>
<g>
<g>
<g>
<path class="st0" d="M280.6,198.7c-15.2,0-31.5,6-31.5,20.6c0,13,14.9,16.8,32.6,19.7c24,3.8,47.6,8.6,47.6,35.6
c-0.2,26.9-25.9,35.6-48.8,35.6c-21.2,0-41.5-7.7-50.7-27.8l12.3-7.2c7.7,14.2,23.8,21.1,38.6,21.1c14.6,0,33.9-4.6,33.9-22.3
c0.2-14.9-16.6-19.2-34.6-21.9c-23.1-3.6-45.6-8.9-45.6-33.2c-0.3-25,25.2-33.6,45.9-33.6c17.8,0,34.8,3.6,45.4,21.8l-11.3,7
C307.9,203.7,294,198.9,280.6,198.7z"/>
<path class="st0" d="M353.8,188.1v49.2c7.2-11.1,18.5-14.9,29.3-15.1c23.8,0,35.5,15.8,35.5,39.1v46.6h-13.9v-46.4
c0-16.6-8.6-26-24-26S354,247.5,354,263v44.9h-14V187.9h13.9V188.1z"/>
<path class="st0" d="M504.4,308.2l-0.3-15.4c-6.7,11.7-19.5,17.1-31.2,17.1c-24.3,0-43.3-16.8-43.3-44.4
c0-27.4,19.4-43.9,43.5-43.7c12.7,0,25.2,5.8,31.4,16.8l0.2-15.4h13.7v84.6h-13.5L504.4,308.2z M473.5,235.2
c-16.8,0-30.3,12-30.3,30.8s13.5,31,30.3,31c40.8,0,40.8-62,0.2-62L473.5,235.2z"/>
<path class="st0" d="M529.5,223.6h13.4l0.7,16.3c6.7-11.3,19.2-17.8,32.6-17.8c24.3,0.5,42.1,17.6,42.1,43.7
c0,26.7-17.6,44-43,44c-12,0-25.4-5.1-32-17.1v55.2h-13.7V223.6z M604.2,266c0-19-12.5-30.3-29.8-30.3c-17.6,0-29.6,13-29.6,30.3
s12.5,30.3,29.6,30.5C591.3,296.5,604.2,285.1,604.2,266z"/>
<path class="st0" d="M708.9,294.3c-8.6,10.1-23.3,15.1-36.5,15.1c-26.2,0-44.5-17.3-44.5-44.2c0-25.5,18.3-43.9,43.9-43.9
c25.9,0,45.6,15.9,42.3,49.7h-72c1.5,15.6,14.4,25.4,30.7,25.4c9.6,0,21.2-3.8,26.9-10.6l9.4,8.6H708.9z M700.6,259.4
c-0.7-16.4-12-25.4-28.6-25.4c-14.7,0-27.6,8.9-30,25.2h58.6V259.4z"/>
</g>
<g>
<path class="st0" d="M771.2,198.7c-15.2,0-31.5,6-31.5,20.6c0,13,14.9,16.8,32.6,19.7c24,3.8,47.6,8.6,47.6,35.6
c-0.2,26.9-25.9,35.6-48.8,35.6c-21.2,0-41.5-7.7-50.7-27.8l12.3-7.2c7.7,14.2,23.8,21.1,38.6,21.1c14.6,0,33.9-4.6,33.9-22.3
c0.2-14.9-16.6-19.2-34.6-21.9c-23.1-3.6-45.6-8.9-45.6-33.2c-0.3-25,25.2-33.6,45.9-33.6c17.8,0,34.8,3.6,45.4,21.8l-11.3,7
C798.5,203.7,784.6,198.9,771.2,198.7z"/>
<path class="st0" d="M844.4,188.1v49.2c7.2-11.1,18.5-14.9,29.3-15.1c23.8,0,35.5,15.8,35.5,39.1v46.6h-13.9v-46.4
c0-16.6-8.6-26-24-26s-26.7,12.2-26.7,27.6v44.9h-14V187.9h13.9V188.1z"/>
<path class="st0" d="M920.6,307.8h14v-83.4h-14V307.8z M927.8,211.7l-11.1-12.2l11.1-12.2l11.1,12.2L927.8,211.7z"/>
<g>
<polygon class="st0" points="960.8,308 960.8,307.8 960.7,307.8 "/>
<path class="st0" d="M974.7,217.6c0-12.7,5.8-18.3,14.9-18.3c0.2,0,0.4,0,0.5,0l3.2-12.1c-1.3-0.2-2.7-0.3-4-0.3
c-17.8,0-28.4,11.3-28.4,30.7v6.7h-16.6v12.3h16.6v71.3h13.9v-71.3h16.8v-12.3h-16.8V217.6z"/>
</g>
<path class="st0" d="M1046.1,296.1c-1.1,0.2-2.2,0.3-3.3,0.3c-10.1,0-13.4-6.3-13.4-16.3v-43.6h18v-12.2h-17.9v-25.8l-14,1.5
v24.2h-17v12.2h17v43.6c0,18.7,8.6,29.3,26.7,29c2.2-0.1,4.4-0.3,6.5-0.8L1046.1,296.1z"/>
</g>
</g>
<polygon class="st1" points="216,82.1 230.5,-0.7 169.9,24.6 103.3,24.6 42.7,-0.8 57.3,82.1 43.6,128.3 56.5,136.4 0,186 0,232.6
64.6,321.6 125.5,342 125.6,342.1 173.2,317.7 173.3,317.6 173.3,281.5 146.3,266.7 146.3,266.7 146.3,266.7 188.7,153.8
188.4,153.8 229.6,128.3 "/>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="136.7364" y1="25.9921" x2="60.3198" y2="247.6647">
<stop offset="0.1345" style="stop-color:#2B415B"/>
<stop offset="0.3762" style="stop-color:#3B5676"/>
<stop offset="0.6923" style="stop-color:#54769E"/>
<stop offset="0.7901" style="stop-color:#52749B"/>
<stop offset="0.8614" style="stop-color:#4D6C92"/>
<stop offset="0.9244" style="stop-color:#436082"/>
<stop offset="0.9822" style="stop-color:#364F6C"/>
<stop offset="1" style="stop-color:#314863"/>
</linearGradient>
<polygon class="st2" points="97.7,100.3 0,186 136.1,264.3 136.6,102.9 "/>
<polygon class="st3" points="83.8,153.3 136.2,293.4 136.6,161.1 "/>
<polygon class="st4" points="188.7,153.8 136.2,293.4 136.6,161.1 "/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="230.1033" y1="127.4219" x2="34.0475" y2="14.229">
<stop offset="0" style="stop-color:#54769E"/>
<stop offset="0.4802" style="stop-color:#53749C"/>
<stop offset="0.6878" style="stop-color:#4F6F95"/>
<stop offset="0.8423" style="stop-color:#486588"/>
<stop offset="0.9095" style="stop-color:#435F80"/>
</linearGradient>
<polygon class="st5" points="230.5,-0.7 178.4,26.7 136.7,35.8 94.6,26.7 42.7,-0.8 60.6,81.9 43.6,128.3 103.2,165.3
136.3,201.3 136.3,201.6 136.5,201.4 136.7,201.6 136.7,201.3 169.8,165.3 229.6,128.3 212.6,82 "/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="342.5284" y1="63.8296" x2="150.4648" y2="63.8296">
<stop offset="0.2539" style="stop-color:#20344C"/>
<stop offset="0.4072" style="stop-color:#273D57"/>
<stop offset="0.6733" style="stop-color:#395373"/>
<stop offset="1" style="stop-color:#54769E"/>
</linearGradient>
<polygon class="st6" points="230.5,-0.7 216,82.1 229.6,128.3 212.6,82 "/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="-74.3281" y1="63.7777" x2="124.2335" y2="63.7777">
<stop offset="0.2539" style="stop-color:#54769E"/>
<stop offset="0.4133" style="stop-color:#4D6E93"/>
<stop offset="0.6897" style="stop-color:#3C5777"/>
<stop offset="1" style="stop-color:#233850"/>
</linearGradient>
<polygon class="st7" points="42.7,-0.8 57.3,82.1 43.6,128.3 60.6,81.9 "/>
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="138.4299" y1="-77.4169" x2="134.5027" y2="85.5632">
<stop offset="6.545247e-03" style="stop-color:#54769E"/>
<stop offset="0.1993" style="stop-color:#507198"/>
<stop offset="0.4502" style="stop-color:#466488"/>
<stop offset="0.7318" style="stop-color:#354F6D"/>
<stop offset="1" style="stop-color:#21354D"/>
</linearGradient>
<polygon class="st8" points="42.7,-0.8 103.3,24.6 169.9,24.6 230.5,-0.7 178.4,26.7 136.7,35.8 94.6,26.7 "/>
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="173.2798" y1="-23.2345" x2="12.7505" y2="132.5687">
<stop offset="0.2539" style="stop-color:#54769E"/>
<stop offset="0.4102" style="stop-color:#4D6E93"/>
<stop offset="0.6813" style="stop-color:#3C5777"/>
<stop offset="1" style="stop-color:#22364E"/>
</linearGradient>
<polygon class="st9" points="60.6,81.9 57.6,90.2 120.5,32.2 "/>
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="114.997" y1="-2.4443" x2="248.7759" y2="116.8474">
<stop offset="0.2539" style="stop-color:#54769E"/>
<stop offset="0.4102" style="stop-color:#4D6E93"/>
<stop offset="0.6813" style="stop-color:#3C5777"/>
<stop offset="1" style="stop-color:#22364E"/>
</linearGradient>
<polygon class="st10" points="212.6,82 153,32.2 215.5,89.8 "/>
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="-31.9333" y1="230.8414" x2="255.118" y2="333.2895">
<stop offset="0.2664" style="stop-color:#54769E"/>
<stop offset="1" style="stop-color:#425E7F"/>
</linearGradient>
<polygon class="st11" points="0,186 146.3,266.7 164.8,313 125.6,327.9 64.6,321.6 0,232.6 "/>
<polygon class="st0" points="121.1,252.8 64.8,321.4 64.6,321.6 125.5,342 125.6,342.1 173.2,317.7 173.3,317.6 173.3,281.5 "/>
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="97.761" y1="-67.9411" x2="268.6103" y2="84.2801">
<stop offset="0.4609" style="stop-color:#54769E;stop-opacity:0"/>
<stop offset="0.5699" style="stop-color:#52739A;stop-opacity:0.2156"/>
<stop offset="0.6764" style="stop-color:#4A698E;stop-opacity:0.4266"/>
<stop offset="0.782" style="stop-color:#3D597B;stop-opacity:0.6356"/>
<stop offset="0.8863" style="stop-color:#2C435F;stop-opacity:0.8422"/>
<stop offset="0.9661" style="stop-color:#1B2E45"/>
</linearGradient>
<polygon class="st12" points="212.6,82 230.5,-0.7 178.4,26.7 153,32.2 "/>
<polygon class="st0" points="136.6,201.6 120.1,183.5 136.6,165.3 153.2,183.5 "/>
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="136.6099" y1="347.9733" x2="136.6099" y2="-96.2296">
<stop offset="0.2539" style="stop-color:#54769E"/>
<stop offset="0.4102" style="stop-color:#4D6E93"/>
<stop offset="0.6813" style="stop-color:#3C5777"/>
<stop offset="1" style="stop-color:#22364E"/>
</linearGradient>
<polygon class="st13" points="135,35.4 136.7,35.8 138.2,35.5 136.6,141 "/>
<path class="st14" d="M77.6,35.5"/>
<path class="st14" d="M75.1,35.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="620px" height="620px" viewBox="0 0 620 620" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>swap</title>
<desc>Created with Sketch.</desc>
<defs>
<circle id="path-1" cx="170" cy="170" r="170"></circle>
<circle id="path-3" cx="170" cy="170" r="170"></circle>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="swap">
<g id="Yellow-Coin" transform="translate(0.000000, 280.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<circle stroke="#0E97C0" stroke-width="20" cx="170" cy="170" r="160"></circle>
<rect id="Rectangle" fill="#FFE14D" mask="url(#mask-2)" x="-1.13333333" y="1.13333333" width="173.4" height="340"></rect>
<rect id="Rectangle" fill="#FFCC33" mask="url(#mask-2)" x="171.133333" y="1.13333333" width="173.4" height="340"></rect>
<circle id="Oval-2" stroke="#333333" stroke-width="20" mask="url(#mask-2)" cx="170" cy="170" r="160"></circle>
<circle id="Oval-3" stroke="#F28618" stroke-width="20" mask="url(#mask-2)" cx="170" cy="170" r="96.3333333"></circle>
</g>
<g id="Blue-Coin" transform="translate(280.000000, 0.000000)">
<mask id="mask-4" fill="white">
<use xlink:href="#path-3"></use>
</mask>
<circle stroke="#0E97C0" stroke-width="20" cx="170" cy="170" r="160"></circle>
<rect id="Rectangle" fill="#6EA6E8" mask="url(#mask-4)" x="-1.13333333" y="1.13333333" width="173.4" height="340"></rect>
<rect id="Rectangle" fill="#5C9BE4" mask="url(#mask-4)" x="171.133333" y="1.13333333" width="173.4" height="340"></rect>
<circle id="Oval-2" stroke="#333333" stroke-width="20" mask="url(#mask-4)" cx="170" cy="170" r="160"></circle>
<circle id="Oval-3" stroke="#2F79CF" stroke-width="20" mask="url(#mask-4)" cx="170" cy="170" r="96.3333333"></circle>
</g>
<g id="Group" transform="translate(226.000000, 320.000000)" fill-rule="nonzero">
<polygon id="Shape" fill="#0492BE" points="311.67 8 385.42 8 385.42 171.31 144.71 171.31 144.71 234.53 13 134.44 144.71 34.34 144.71 97.56 311.67 97.56"></polygon>
<polygon id="Shape" fill="#103957" opacity="0.2" points="348.54 134.44 13 134.44 144.71 234.53 144.71 171.31 385.42 171.31 385.42 8 348.54 8"></polygon>
<path d="M152.66,250.36 L0,134.36 L152.66,18.36 L152.66,89.61 L303.82,89.61 L303.82,0 L393.38,0 L393.38,179.11 L152.66,179.11 L152.66,250.36 Z M26.11,134.36 L136.85,218.52 L136.85,163.31 L377.58,163.31 L377.58,15.8 L319.63,15.8 L319.63,105.36 L136.86,105.36 L136.86,50.17 L26.11,134.36 Z" id="Shape" fill="#000000"></path>
</g>
<g id="Group" transform="translate(197.000000, 174.500000) rotate(180.000000) translate(-197.000000, -174.500000) translate(0.000000, 49.000000)" fill-rule="nonzero">
<polygon id="Shape" fill="#0492BE" points="311.67 8 385.42 8 385.42 171.31 144.71 171.31 144.71 234.53 13 134.44 144.71 34.34 144.71 97.56 311.67 97.56"></polygon>
<polygon id="Shape" fill="#103957" opacity="0.2" points="348.54 134.44 13 134.44 144.71 234.53 144.71 171.31 385.42 171.31 385.42 8 348.54 8"></polygon>
<path d="M152.66,250.36 L0,134.36 L152.66,18.36 L152.66,89.61 L303.82,89.61 L303.82,0 L393.38,0 L393.38,179.11 L152.66,179.11 L152.66,250.36 Z M26.11,134.36 L136.85,218.52 L136.85,163.31 L377.58,163.31 L377.58,15.8 L319.63,15.8 L319.63,105.36 L136.86,105.36 L136.86,50.17 L26.11,134.36 Z" id="Shape" fill="#000000"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1,18 @@
import React from 'react';
import { AddressFieldFactory } from './AddressFieldFactory';
import { donationAddressMap } from 'config/data';
export const AddressField: React.SFC<{}> = () => (
<AddressFieldFactory
withProps={({ currentTo, isValid, onChange, readOnly }) => (
<input
className={`form-control ${isValid ? 'is-valid' : 'is-invalid'}`}
type="text"
value={currentTo.raw}
placeholder={donationAddressMap.ETH}
readOnly={!!readOnly}
onChange={onChange}
/>
)}
/>
);

View File

@ -1 +0,0 @@
export * from './AddressField';

View File

@ -1,8 +1,9 @@
import { Query } from 'components/renderCbs';
import { setCurrentTo, TSetCurrentTo } from 'actions/transaction';
import { AddressInput } from './AddressInput';
import { AddressInputFactory } from './AddressInputFactory';
import React from 'react';
import { connect } from 'react-redux';
import { ICurrentTo } from 'selectors/transaction';
interface DispatchProps {
setCurrentTo: TSetCurrentTo;
@ -10,12 +11,20 @@ interface DispatchProps {
interface OwnProps {
to: string | null;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
export interface CallbackProps {
isValid: boolean;
readOnly: boolean;
currentTo: ICurrentTo;
onChange(ev: React.FormEvent<HTMLInputElement>): void;
}
type Props = DispatchProps & DispatchProps & OwnProps;
//TODO: add ens resolving
class AddressFieldClass extends React.Component<Props, {}> {
class AddressFieldFactoryClass extends React.Component<Props, {}> {
public componentDidMount() {
// this 'to' parameter can be either token or actual field related
const { to } = this.props;
@ -25,7 +34,7 @@ class AddressFieldClass extends React.Component<Props, {}> {
}
public render() {
return <AddressInput onChange={this.setAddress} />;
return <AddressInputFactory onChange={this.setAddress} withProps={this.props.withProps} />;
}
private setAddress = (ev: React.FormEvent<HTMLInputElement>) => {
@ -34,10 +43,14 @@ class AddressFieldClass extends React.Component<Props, {}> {
};
}
const AddressField = connect(null, { setCurrentTo })(AddressFieldClass);
const AddressField = connect(null, { setCurrentTo })(AddressFieldFactoryClass);
const DefaultAddressField: React.SFC<{}> = () => (
<Query params={['to']} withQuery={({ to }) => <AddressField to={to} />} />
interface DefaultAddressFieldProps {
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
const DefaultAddressField: React.SFC<DefaultAddressFieldProps> = ({ withProps }) => (
<Query params={['to']} withQuery={({ to }) => <AddressField to={to} withProps={withProps} />} />
);
export { DefaultAddressField as AddressField };
export { DefaultAddressField as AddressFieldFactory };

View File

@ -3,10 +3,10 @@ import { Identicon } from 'components/ui';
import translate from 'translations';
//import { EnsAddress } from './components';
import { Query } from 'components/renderCbs';
import { donationAddressMap } from 'config/data';
import { ICurrentTo, getCurrentTo, isValidCurrentTo } from 'selectors/transaction';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { CallbackProps } from 'components/AddressFieldFactory';
interface StateProps {
currentTo: ICurrentTo;
@ -14,14 +14,15 @@ interface StateProps {
}
interface OwnProps {
onChange(ev: React.FormEvent<HTMLInputElement>): void;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
type Props = OwnProps & StateProps;
//TODO: ENS handling
class AddressInputClass extends Component<Props> {
class AddressInputFactoryClass extends Component<Props> {
public render() {
const { currentTo, onChange, isValid } = this.props;
const { currentTo, onChange, isValid, withProps } = this.props;
const { raw } = currentTo;
return (
<div className="row form-group">
@ -29,16 +30,9 @@ class AddressInputClass extends Component<Props> {
<label>{translate('SEND_addr')}:</label>
<Query
params={['readOnly']}
withQuery={({ readOnly }) => (
<input
className={`form-control ${isValid ? 'is-valid' : 'is-invalid'}`}
type="text"
value={raw}
placeholder={donationAddressMap.ETH}
readOnly={!!readOnly}
onChange={onChange}
/>
)}
withQuery={({ readOnly }) =>
withProps({ currentTo, isValid, onChange, readOnly: !!readOnly })
}
/>
{/*<EnsAddress ensAddress={ensAddress} />*/}
</div>
@ -50,7 +44,7 @@ class AddressInputClass extends Component<Props> {
}
}
export const AddressInput = connect((state: AppState) => ({
export const AddressInputFactory = connect((state: AppState) => ({
currentTo: getCurrentTo(state),
isValid: isValidCurrentTo(state)
}))(AddressInputClass);
}))(AddressInputFactoryClass);

View File

@ -0,0 +1 @@
export * from './AddressFieldFactory';

View File

@ -44,3 +44,17 @@
}
}
}
#NAV_Swap a:before {
content:"";
display: inline-block;
margin-top: -.1rem;
width: 1.3rem;
height: 1.3rem;
background-image: url('~assets/images/swap.svg');
background-position: center;
background-repeat: no-repeat;
background-size: contain;
vertical-align: middle;
margin-right: 4px;
}

View File

@ -44,7 +44,11 @@ class NavigationLink extends React.Component<Props, {}> {
</Link>
);
return <li className="NavigationLink">{linkEl}</li>;
return (
<li id={link.name} className="NavigationLink">
{linkEl}
</li>
);
}
}

View File

@ -0,0 +1,112 @@
@import 'common/sass/variables';
.SwapDropdown {
position: relative;
button {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
border: 1px solid #ccc;
padding: 0.4rem 1rem;
border-radius: 2px;
&:focus {
outline: none;
}
&:active, &:hover {
opacity: 0.8;
}
> li {
margin: 0;
&:first-child {
padding-top: 4px;
}
&:last-child {
padding-bottom: 4px;
}
> a {
font-weight: 300;
&.active {
color: $link-color;
}
}
}
}
}
.SwapDropdown-grid {
position: absolute;
display: none;
padding: 0;
margin-bottom: 0;
min-width: 500px;
left: 50%;
top: 50px;
transform: translateX(-50%);
list-style: none;
font-size: 0.8rem;
text-align: left;
z-index: 500;
background: white;
box-shadow: 2px 1px 60px rgba(0,0,0,.175);
&::before {
content: "";
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
border-right: 10px solid transparent;
border-left: 10px solid transparent;
border-top: 10px solid transparent;
border-bottom: 10px solid #fff;
}
&.open {
display: block;
}
li {
display: inline-block;
width: 33.3%;
margin-bottom: 0;
}
li > a {
display: block;
clear: both;
padding: 5px 20px;
color: #163151;
&:hover {
opacity: .8;
background-color: #163151;
color: #fff;
}
}
.inactive {
a {
color: grey;
&:hover {
background-color: #fff;
color:#163151;
cursor: not-allowed;
}
}
img {
filter: grayscale(100%);
}
}
strong {
margin-left: 5px;
}
@media screen and (max-width: 800px) {
min-width: 300px;
}
}
.SwapDropdown-desc {
display: inline-block;
}
.SwapDropdown-item {
position: relative;
img {
padding-right: 1px;
}
}

View File

@ -0,0 +1,100 @@
import React, { Component } from 'react';
import './SwapDropdown.scss';
import classnames from 'classnames';
export interface SingleCoin {
id: string;
name: string;
image: string;
status: string;
}
interface Props<T> {
ariaLabel: string;
options: SingleCoin[];
value: string;
onChange(value: T): void;
}
class SwapDropdown<T> extends Component<Props<T>, {}> {
public state = {
open: false
};
private dropdown: HTMLElement | null;
public componentDidMount() {
document.addEventListener('click', this.clickHandler);
}
public componentWillUnmount() {
document.removeEventListener('click', this.clickHandler);
}
public handleClickOutside() {
this.toggleDropdown();
}
public render() {
const { open } = this.state;
const { options, value } = this.props;
const dropdownGrid = classnames(open && 'open', 'SwapDropdown-grid');
const mappedCoins = options.sort((a, b) => (a.id > b.id ? 1 : -1)).map((coin: SingleCoin) => {
const cn = classnames(coin.status !== 'available' && 'inactive', 'SwapDropdown-item');
return (
<li className={cn} key={coin.id}>
<a onClick={coin.status === 'available' ? this.onChange.bind(null, coin.id) : null}>
<img src={coin.image} height="20" width="20" />
{/* <div className="SwapDropdown-desc"> */}
<strong>{coin.id}</strong>
<br />
<small>{coin.name}</small>
{/* </div> */}
</a>
</li>
);
});
return (
<div className="SwapDropdown" ref={el => (this.dropdown = el)}>
<button onClick={this.toggleDropdown}>
{value}
<i className="caret" />
</button>
<ul className={dropdownGrid}>{mappedCoins}</ul>
</div>
);
}
private toggleDropdown = () => {
this.setState({
open: !this.state.open
});
};
private onChange = (value: any) => {
this.props.onChange(value);
if (this.state.open) {
this.setState({
open: false
});
}
};
private clickHandler = (ev: Event) => {
if (!this.state.open || !this.dropdown) {
return;
}
if (
this.dropdown !== ev.target &&
ev.target instanceof HTMLElement &&
!this.dropdown.contains(ev.target)
) {
this.setState({
open: false
});
}
};
}
export default SwapDropdown;

View File

@ -8,6 +8,7 @@ export { default as QRCode } from './QRCode';
export { default as NewTabLink } from './NewTabLink';
export { default as UnitDisplay } from './UnitDisplay';
export { default as Spinner } from './Spinner';
export { default as SwapDropdown } from './SwapDropdown';
export { default as Tooltip } from './Tooltip';
export * from './ConditionalInput';
export * from './Aux';

View File

@ -46,6 +46,8 @@ export const MINIMUM_PASSWORD_LENGTH = 9;
export const knowledgeBaseURL = 'https://myetherwallet.github.io/knowledge-base';
export const bityReferralURL = 'https://bity.com/af/jshkb37v';
// Note: add the real referral url once you know it
export const shapeshiftReferralURL = 'https://shapeshift.io';
export const ledgerReferralURL = 'https://www.ledgerwallet.com/r/fa4b?path=/products/';
export const trezorReferralURL = 'https://trezor.io/?a=myetherwallet.com';
export const bitboxReferralURL = 'https://digitalbitbox.com/?ref=mew';

View File

@ -54,3 +54,6 @@
margin-top: $space * 2.5;
}
}

View File

@ -1,34 +1,42 @@
import { TChangeStepSwap, TInitSwap } from 'actions/swap';
import { NormalizedBityRates, NormalizedOptions, SwapInput } from 'reducers/swap/types';
import { TChangeStepSwap, TInitSwap, TChangeSwapProvider, ProviderName } from 'actions/swap';
import {
NormalizedBityRates,
NormalizedShapeshiftRates,
NormalizedOptions,
SwapInput
} from 'reducers/swap/types';
import SimpleButton from 'components/ui/SimpleButton';
import bityConfig, { generateKindMax, generateKindMin, WhitelistedCoins } from 'config/bity';
import React, { Component } from 'react';
import translate from 'translations';
import { combineAndUpper } from 'utils/formatters';
import { Dropdown } from 'components/ui';
import { SwapDropdown } from 'components/ui';
import Spinner from 'components/ui/Spinner';
import intersection from 'lodash/intersection';
import without from 'lodash/without';
import { merge, reject, debounce } from 'lodash';
import './CurrencySwap.scss';
export interface StateProps {
bityRates: NormalizedBityRates;
shapeshiftRates: NormalizedShapeshiftRates;
provider: ProviderName;
options: NormalizedOptions;
}
export interface ActionProps {
changeStepSwap: TChangeStepSwap;
initSwap: TInitSwap;
swapProvider: TChangeSwapProvider;
}
interface State {
disabled: boolean;
origin: SwapInput;
destination: SwapInput;
originKindOptions: WhitelistedCoins[];
destinationKindOptions: WhitelistedCoins[];
originKindOptions: any[];
destinationKindOptions: any[];
originErr: string;
destinationErr: string;
timeout: boolean;
}
type Props = StateProps & ActionProps;
@ -36,26 +44,63 @@ type Props = StateProps & ActionProps;
export default class CurrencySwap extends Component<Props, State> {
public state = {
disabled: true,
origin: { id: 'BTC', amount: NaN } as SwapInput,
destination: { id: 'ETH', amount: NaN } as SwapInput,
originKindOptions: ['BTC', 'ETH'] as WhitelistedCoins[],
destinationKindOptions: ['ETH'] as WhitelistedCoins[],
origin: {
id: 'BTC',
status: 'available',
image: 'https://shapeshift.io/images/coins/bitcoin.png',
amount: NaN
} as SwapInput,
destination: {
id: 'ETH',
status: 'available',
image: 'https://shapeshift.io/images/coins/ether.png',
amount: NaN
} as SwapInput,
originKindOptions: [],
destinationKindOptions: [],
originErr: '',
destinationErr: ''
destinationErr: '',
timeout: false
};
public componentDidUpdate(prevProps: Props, prevState: State) {
const { origin, destination } = this.state;
public debouncedCreateErrString = debounce((origin, destination, showError) => {
const createErrString = (
originKind: WhitelistedCoins,
amount: number,
destKind: WhitelistedCoins
) => {
const rate = this.getMinMax(originKind, destKind);
let errString;
if (amount > rate.max) {
errString = `Maximum ${rate.max} ${originKind}`;
} else {
errString = `Minimum ${rate.min} ${originKind}`;
}
return errString;
};
const originErr = showError ? createErrString(origin.id, origin.amount, destination.id) : '';
const destinationErr = showError
? createErrString(destination.id, destination.amount, origin.id)
: '';
this.setErrorMessages(originErr, destinationErr);
}, 1000);
public componentDidMount() {
setTimeout(() => {
this.setState({
timeout: true
});
}, 10000);
const { origin } = this.state;
const { options } = this.props;
if (origin !== prevState.origin) {
this.setDisabled(origin, destination);
}
if (options.allIds !== prevProps.options.allIds) {
const originKindOptions: WhitelistedCoins[] = intersection<any>(
options.allIds,
this.state.originKindOptions
if (options.allIds && options.byId) {
const originKindOptions: any[] = Object.values(options.byId);
const destinationKindOptions: any[] = Object.values(
reject<any>(options.byId, o => o.id === origin.id)
);
const destinationKindOptions: WhitelistedCoins[] = without<any>(options.allIds, origin.id);
this.setState({
originKindOptions,
destinationKindOptions
@ -63,54 +108,110 @@ export default class CurrencySwap extends Component<Props, State> {
}
}
public getMinMax = (kind: WhitelistedCoins) => {
public componentDidUpdate(prevProps: Props, prevState: State) {
const { origin, destination } = this.state;
const { options, bityRates, shapeshiftRates } = this.props;
if (origin !== prevState.origin) {
this.setDisabled(origin, destination);
}
const originCap = origin.id.toUpperCase();
const destCap = destination.id.toUpperCase();
const { provider } = this.props;
const ensureCorrectProvider =
(originCap === 'BTC' && destCap === 'ETH') || (destCap === 'BTC' && originCap === 'ETH');
const ensureBityRatesLoaded =
bityRates.allIds.includes('ETHBTC') && bityRates.allIds.includes('BTCETH');
const ensureShapeshiftRatesLoaded = shapeshiftRates.allIds.length > 0;
if (ensureBityRatesLoaded && ensureCorrectProvider) {
if (provider === 'shapeshift') {
this.props.swapProvider('bity');
}
} else if (ensureShapeshiftRatesLoaded) {
if (provider !== 'shapeshift') {
this.props.swapProvider('shapeshift');
}
}
if (options.allIds !== prevProps.options.allIds && options.byId) {
const originKindOptions: any[] = Object.values(options.byId);
const destinationKindOptions: any[] = Object.values(
reject<any>(options.byId, o => o.id === origin.id)
);
this.setState({
originKindOptions,
destinationKindOptions
});
}
}
public rateMixer = () => {
const { shapeshiftRates, bityRates } = this.props;
return merge(shapeshiftRates, bityRates);
};
public getMinMax = (originKind: WhitelistedCoins, destinationKind) => {
let min;
let max;
if (kind !== 'BTC') {
const bityPairRate = this.props.bityRates.byId['BTC' + kind].rate;
min = generateKindMin(bityPairRate, kind);
max = generateKindMax(bityPairRate, kind);
const { provider, bityRates } = this.props;
if (provider === 'bity' && bityRates.allIds.length > 2) {
if (originKind !== 'BTC') {
const pairRate = this.rateMixer().byId['BTC' + originKind].rate;
min = generateKindMin(pairRate, originKind);
max = generateKindMax(pairRate, originKind);
} else {
min = bityConfig.BTCMin;
max = bityConfig.BTCMax;
}
} else {
min = bityConfig.BTCMin;
max = bityConfig.BTCMax;
const pair = (this.rateMixer() as NormalizedShapeshiftRates).byId[
originKind + destinationKind
];
min = pair.min;
max = pair.limit;
}
return { min, max };
};
public isMinMaxValid = (amount: number, kind: WhitelistedCoins) => {
const rate = this.getMinMax(kind);
const higherThanMin = amount >= rate.min;
const lowerThanMax = amount <= rate.max;
public isMinMaxValid = (originAmount: number, originKind: WhitelistedCoins, destinationKind) => {
const rate = this.getMinMax(originKind, destinationKind);
const higherThanMin = originAmount >= rate.min;
const lowerThanMax = originAmount <= rate.max;
return higherThanMin && lowerThanMax;
};
public setDisabled(origin: SwapInput, destination: SwapInput) {
this.clearErrMessages();
const amountsValid = origin.amount && destination.amount;
const minMaxValid = this.isMinMaxValid(origin.amount, origin.id);
const minMaxValid = this.isMinMaxValid(origin.amount as number, origin.id, destination.id);
const disabled = !(amountsValid && minMaxValid);
const createErrString = (kind: WhitelistedCoins, amount: number) => {
const rate = this.getMinMax(kind);
let errString;
if (amount > rate.max) {
errString = `Maximum ${rate.max} ${kind}`;
} else {
errString = `Minimum ${rate.min} ${kind}`;
}
return errString;
};
const showError = disabled && amountsValid;
const originErr = showError ? createErrString(origin.id, origin.amount) : '';
const destinationErr = showError ? createErrString(destination.id, destination.amount) : '';
this.setState({
disabled,
disabled
});
this.debouncedCreateErrString(origin, destination, showError);
}
public setErrorMessages = (originErr, destinationErr) => {
this.setState({
originErr,
destinationErr
});
}
};
public clearErrMessages = () => {
this.setState({
originErr: '',
destinationErr: ''
});
};
public onClickStartSwap = () => {
const { origin, destination } = this.state;
@ -129,8 +230,8 @@ export default class CurrencySwap extends Component<Props, State> {
public updateOriginAmount = (origin: SwapInput, destination: SwapInput, amount: number) => {
if (amount || amount === 0) {
const pairName = combineAndUpper(origin.id, destination.id);
const bityRate = this.props.bityRates.byId[pairName].rate;
const destinationAmount = amount * bityRate;
const rate = this.rateMixer().byId[pairName].rate;
const destinationAmount = amount * rate;
this.setState({
origin: { ...this.state.origin, amount },
destination: { ...this.state.destination, amount: destinationAmount }
@ -143,8 +244,8 @@ export default class CurrencySwap extends Component<Props, State> {
public updateDestinationAmount = (origin: SwapInput, destination: SwapInput, amount: number) => {
if (amount || amount === 0) {
const pairNameReversed = combineAndUpper(destination.id, origin.id);
const bityRate = this.props.bityRates.byId[pairNameReversed].rate;
const originAmount = amount * bityRate;
const rate = this.rateMixer().byId[pairNameReversed].rate;
const originAmount = amount * rate;
this.setState({
origin: { ...this.state.origin, amount: originAmount },
destination: {
@ -168,69 +269,80 @@ export default class CurrencySwap extends Component<Props, State> {
public onChangeOriginKind = (newOption: WhitelistedCoins) => {
const { origin, destination, destinationKindOptions } = this.state;
const newDestinationAmount = () => {
const pairName = combineAndUpper(destination.id, origin.id);
const bityRate = this.props.bityRates.byId[pairName].rate;
return bityRate * origin.amount;
const { options, initSwap } = this.props;
const newOrigin = { ...origin, id: newOption, amount: '' };
const newDest = {
id: newOption === destination.id ? origin.id : destination.id,
amount: ''
};
this.setState({
origin: { ...origin, id: newOption },
destination: {
id: newOption === destination.id ? origin.id : destination.id,
amount: newDestinationAmount() ? newDestinationAmount() : destination.amount
},
destinationKindOptions: without([...destinationKindOptions, origin.id], newOption)
origin: newOrigin,
destination: newDest,
destinationKindOptions: reject(
[...destinationKindOptions, options.byId[origin.id]],
o => o.id === newOption
)
});
initSwap({ origin: newOrigin, destination: newDest });
};
public onChangeDestinationKind = (newOption: WhitelistedCoins) => {
const { initSwap } = this.props;
const { origin, destination } = this.state;
const newOriginAmount = () => {
const pairName = combineAndUpper(newOption, origin.id);
const bityRate = this.props.bityRates.byId[pairName].rate;
return bityRate * destination.amount;
const newOrigin = {
...origin,
amount: ''
};
const newDest = { ...destination, id: newOption, amount: '' };
this.setState({
origin: {
...origin,
amount: newOriginAmount() ? newOriginAmount() : origin.amount
},
destination: { ...destination, id: newOption }
origin: newOrigin,
destination: newDest
});
initSwap({ origin: newOrigin, destination: newDest });
};
public render() {
const { bityRates } = this.props;
const { bityRates, shapeshiftRates, provider } = this.props;
const {
origin,
destination,
originKindOptions,
destinationKindOptions,
originErr,
destinationErr
destinationErr,
timeout
} = this.state;
const OriginKindDropDown = Dropdown as new () => Dropdown<any>;
const DestinationKindDropDown = Dropdown as new () => Dropdown<typeof destination.id>;
const OriginKindDropDown = SwapDropdown as new () => SwapDropdown<any>;
const DestinationKindDropDown = SwapDropdown as new () => SwapDropdown<any>;
const pairName = combineAndUpper(origin.id, destination.id);
const bityLoaded = bityRates.byId[pairName] ? bityRates.byId[pairName].id : false;
const bityLoaded = bityRates.byId && bityRates.byId[pairName] ? true : false;
const shapeshiftLoaded = shapeshiftRates.byId && shapeshiftRates.byId[pairName] ? true : false;
// This ensures both are loaded
const loaded = provider === 'shapeshift' ? shapeshiftLoaded : bityLoaded && shapeshiftLoaded;
const timeoutLoaded = (bityLoaded && timeout) || (shapeshiftLoaded && timeout);
return (
<article className="CurrencySwap">
<h1 className="CurrencySwap-title">{translate('SWAP_init_1')}</h1>
{bityLoaded ? (
{loaded || timeoutLoaded ? (
<div className="form-inline CurrencySwap-inner-wrap">
<div className="CurrencySwap-input-group">
{originErr && <span className="CurrencySwap-error-message">{originErr}</span>}
<input
id="origin-swap-input"
className={`CurrencySwap-input form-control ${
String(origin.amount) !== '' && this.isMinMaxValid(origin.amount, origin.id)
String(origin.amount) !== '' &&
this.isMinMaxValid(origin.amount as number, origin.id, destination.id)
? 'is-valid'
: 'is-invalid'
}`}
type="number"
placeholder="Amount"
value={isNaN(origin.amount) ? '' : origin.amount}
value={isNaN(origin.amount as number) ? '' : origin.amount}
onChange={this.onChangeAmount}
/>
<div className="CurrencySwap-dropdown">
@ -239,7 +351,6 @@ export default class CurrencySwap extends Component<Props, State> {
options={originKindOptions}
value={origin.id}
onChange={this.onChangeOriginKind}
color="default"
/>
</div>
</div>
@ -251,13 +362,14 @@ export default class CurrencySwap extends Component<Props, State> {
<input
id="destination-swap-input"
className={`CurrencySwap-input form-control ${
String(destination.amount) !== '' && this.isMinMaxValid(origin.amount, origin.id)
String(destination.amount) !== '' &&
this.isMinMaxValid(origin.amount as number, origin.id, destination.id)
? 'is-valid'
: 'is-invalid'
}`}
type="number"
placeholder="Amount"
value={isNaN(destination.amount) ? '' : destination.amount}
value={isNaN(destination.amount as number) ? '' : destination.amount}
onChange={this.onChangeAmount}
/>
<div className="CurrencySwap-dropdown">
@ -266,7 +378,6 @@ export default class CurrencySwap extends Component<Props, State> {
options={destinationKindOptions}
value={destination.id}
onChange={this.onChangeDestinationKind}
color="default"
/>
</div>
</div>

View File

@ -9,7 +9,7 @@
&-panel {
position: relative;
margin: 0 auto $space * 2;
margin: 0 auto 0;
background: linear-gradient(150deg, $ether-blue, $ether-navy);
@include mono;

View File

@ -1,84 +1,136 @@
import { NormalizedBityRate } from 'reducers/swap/types';
import {
NormalizedBityRates,
NormalizedShapeshiftRates,
NormalizedShapeshiftRate
} from 'reducers/swap/types';
import bityLogoWhite from 'assets/images/logo-bity-white.svg';
import shapeshiftLogoWhite from 'assets/images/logo-shapeshift.svg';
import Spinner from 'components/ui/Spinner';
import { bityReferralURL } from 'config/data';
import { bityReferralURL, shapeshiftReferralURL } from 'config/data';
import React, { Component } from 'react';
import translate from 'translations';
import { toFixedIfLarger } from 'utils/formatters';
import './CurrentRates.scss';
import { SHAPESHIFT_WHITELIST } from 'api/shapeshift';
import { ProviderName } from 'actions/swap';
import sample from 'lodash/sample';
import times from 'lodash/times';
import Rates from './Rates';
interface Props {
[id: string]: NormalizedBityRate;
provider: ProviderName;
bityRates: NormalizedBityRates;
shapeshiftRates: NormalizedShapeshiftRates;
}
interface State {
ETHBTCAmount: number;
ETHREPAmount: number;
BTCETHAmount: number;
BTCREPAmount: number;
}
export default class CurrentRates extends Component<Props> {
private shapeShiftRateCache = null;
export default class CurrentRates extends Component<Props, State> {
public state = {
ETHBTCAmount: 1,
ETHREPAmount: 1,
BTCETHAmount: 1,
BTCREPAmount: 1
public getRandomSSPairData = (
shapeshiftRates: NormalizedShapeshiftRates
): NormalizedShapeshiftRate => {
const coinOne = sample(SHAPESHIFT_WHITELIST) as string;
const coinTwo = sample(SHAPESHIFT_WHITELIST) as string;
const pair = coinOne + coinTwo;
const pairData = shapeshiftRates.byId[pair];
if (pairData) {
return pairData;
} else {
// if random pairing is unavailable / missing in state
return this.getRandomSSPairData(shapeshiftRates);
}
};
public onChange = (event: any) => {
const { value } = event.target;
const { name } = event.target;
this.setState({
[name]: value
});
public buildSSPairs = (shapeshiftRates: NormalizedShapeshiftRates, n: number = 4) => {
const pairCollection = times(n, () => this.getRandomSSPairData(shapeshiftRates));
const byId = pairCollection.reduce((acc, cur) => {
acc[cur.id] = cur;
return acc;
}, {});
const allIds = pairCollection.map(SSData => SSData.id);
return {
byId,
allIds
};
};
public buildPairRate = (origin: string, destination: string) => {
const pair = origin + destination;
const statePair = this.state[(pair + 'Amount') as keyof State];
const propsPair = this.props[pair] ? this.props[pair].rate : null;
return (
<div className="SwapRates-panel-rate">
{propsPair ? (
<div>
<input
className="SwapRates-panel-rate-input"
onChange={this.onChange}
value={statePair}
name={pair + 'Amount'}
/>
<span className="SwapRates-panel-rate-amount">
{` ${origin} = ${toFixedIfLarger(statePair * propsPair, 6)} ${destination}`}
</span>
</div>
) : (
<Spinner size="x1" light={true} />
)}
</div>
);
public isValidRates = rates => {
return rates && rates.allIds && rates.allIds.length > 0;
};
public render() {
public setupRates = () => {
const { shapeshiftRates, bityRates, provider } = this.props;
let fixedRates;
if (provider === 'bity') {
fixedRates = bityRates;
} else if (provider === 'shapeshift') {
// if ShapeShift rates are valid, filter to 4 random pairs
if (this.isValidRates(shapeshiftRates)) {
if (!this.shapeShiftRateCache) {
fixedRates = this.buildSSPairs(shapeshiftRates);
this.shapeShiftRateCache = fixedRates;
} else {
fixedRates = this.shapeShiftRateCache;
}
} else {
// else, pass along invalid rates. Child component will handle showing spinner until they become valid
fixedRates = shapeshiftRates;
}
}
return fixedRates;
};
public swapEl = (providerURL, providerLogo, children) => {
return (
<article className="SwapRates">
<h3 className="SwapRates-title">{translate('SWAP_rates')}</h3>
<section className="SwapRates-panel row">
<div className="SwapRates-panel-side col-sm-6">
{this.buildPairRate('ETH', 'BTC')}
{this.buildPairRate('ETH', 'REP')}
</div>
<div className="SwapRates-panel-side col-sm-6">
{this.buildPairRate('BTC', 'ETH')}
{this.buildPairRate('BTC', 'REP')}
</div>
<a className="SwapRates-panel-logo" href={bityReferralURL} target="_blank">
<img src={bityLogoWhite} width={120} height={49} />
{children}
<a className="SwapRates-panel-logo" href={providerURL} target="_blank">
<img src={providerLogo} width={120} height={49} />
</a>
</section>
</article>
);
};
public render() {
const { provider } = this.props;
const rates = this.setupRates();
const providerLogo = provider === 'shapeshift' ? shapeshiftLogoWhite : bityLogoWhite;
const providerURL = provider === 'shapeshift' ? shapeshiftReferralURL : bityReferralURL;
let children;
if (this.isValidRates(rates)) {
children = <Rates provider={provider} rates={rates} />;
} else {
// TODO - de-dup
children = (
<>
<div className="SwapRates-panel-side col-sm-6">
<div className="SwapRates-panel-rate">
<Spinner size="x1" light={true} />
</div>
<div className="SwapRates-panel-rate">
<Spinner size="x1" light={true} />
</div>
</div>
<div className="SwapRates-panel-side col-sm-6">
<div className="SwapRates-panel-rate">
<Spinner size="x1" light={true} />
</div>
<div className="SwapRates-panel-rate">
<Spinner size="x1" light={true} />
</div>
</div>
</>
);
}
return this.swapEl(providerURL, providerLogo, children);
}
}

View File

@ -0,0 +1,100 @@
import React, { Component } from 'react';
import { AmountFieldFactory } from 'components/AmountFieldFactory';
import { GasFieldFactory } from 'components/GasFieldFactory';
import { AddressFieldFactory } from 'components/AddressFieldFactory';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { Aux } from 'components/ui';
import { GenerateTransaction, SendButton, SigningStatus } from 'components';
import { resetWallet, TResetWallet } from 'actions/wallet';
import translate from 'translations';
import { getUnit } from 'selectors/transaction';
interface StateProps {
unit: string;
resetWallet: TResetWallet;
}
type Props = StateProps;
class FieldsClass extends Component<Props> {
public render() {
return (
<div className="Tab-content-pane">
<div className="row form-group">
<div className="col-xs-12">
<button
className="Deploy-field-reset btn btn-default btn-sm"
onClick={this.changeWallet}
>
<i className="fa fa-refresh" />
{translate('Change Wallet')}
</button>
</div>
<div className="col-xs-12">
<AddressFieldFactory
withProps={({ currentTo }) => (
<input className="form-control" type="text" value={currentTo.raw} readOnly={true} />
)}
/>
</div>
<div className="col-xs-1" />
</div>
<div className="row form-group">
<div className="col-xs-12">
<label>{translate('SEND_amount')}</label>
<AmountFieldFactory
withProps={({ currentValue, isValid }) => (
<Aux>
{!isValid && (
<h5 style={{ color: 'red' }}>
WARNING: Your ether or token balance is not high enough to complete this
transaction! Please send more funds or switch to a different wallet
</h5>
)}
{isValid && (
<input
className="form-control"
type="text"
value={`${currentValue.raw} ${this.props.unit}`}
readOnly={true}
/>
)}
</Aux>
)}
/>
</div>
</div>
<div className="row form-group">
<div className="col-xs-12">
<label>{translate('TRANS_gas')} </label>
<GasFieldFactory
withProps={({ gasLimit }) => (
<input className="form-control" type="text" value={gasLimit.raw} readOnly={true} />
)}
/>
</div>
</div>
<SigningStatus />
<div className="row form-group">
<div className="col-xs-12 clearfix">
<GenerateTransaction />
</div>
</div>
<div className="row form-group">
<SendButton />
</div>
</div>
);
}
private changeWallet = () => {
this.props.resetWallet();
};
}
export const Fields = connect((state: AppState) => ({ unit: getUnit(state) }), { resetWallet })(
FieldsClass
);

View File

@ -0,0 +1,62 @@
import React, { Component } from 'react';
import WalletDecrypt from 'components/WalletDecrypt';
import { OnlyUnlocked } from 'components/renderCbs';
import { Aux } from 'components/ui';
import { Fields } from './Fields';
import { isUnlocked as isUnlockedSelector } from 'selectors/wallet';
import { getNetworkConfig } from 'selectors/config';
import { configureLiteSend, TConfigureLiteSend } from 'actions/swap';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { shouldDisplayLiteSend } from 'selectors/swap';
import { NetworkConfig } from 'config/data';
interface DispatchProps {
configureLiteSend: TConfigureLiteSend;
}
interface StateProps {
shouldDisplay: boolean;
isUnlocked: boolean;
network: NetworkConfig;
}
type Props = StateProps & DispatchProps;
class LiteSendClass extends Component<Props> {
public componentDidMount() {
this.props.configureLiteSend();
}
public render() {
if (!this.props.shouldDisplay) {
return null;
}
const { network, isUnlocked } = this.props;
let renderMe;
if (network.chainId !== 1) {
renderMe = (
<div className="row">
<div className="col-xs-8 col-xs-push-2 text-center">
<h5 style={{ color: 'red' }}>
WARNING: You are currently not on the Ethereum Mainnet. Please switch nodes in order
for the token swap to function as intended.
</h5>
</div>
</div>
);
} else {
renderMe = isUnlocked ? <OnlyUnlocked whenUnlocked={<Fields />} /> : <WalletDecrypt />;
}
return <Aux>{renderMe}</Aux>;
}
}
export const LiteSend = connect(
(state: AppState) => ({
shouldDisplay: shouldDisplayLiteSend(state),
isUnlocked: isUnlockedSelector(state),
network: getNetworkConfig(state)
}),
{ configureLiteSend }
)(LiteSendClass);

View File

@ -0,0 +1 @@
export * from './LiteSend';

View File

@ -3,14 +3,17 @@ import {
TRestartSwap,
TStartOrderTimerSwap,
TStartPollBityOrderStatus,
TStartPollShapeshiftOrderStatus,
TStopOrderTimerSwap,
TStopPollBityOrderStatus
TStopPollBityOrderStatus,
TStopPollShapeshiftOrderStatus
} from 'actions/swap';
import { SwapInput } from 'reducers/swap/types';
import React, { Component } from 'react';
import BitcoinQR from './BitcoinQR';
import PaymentInfo from './PaymentInfo';
import SwapProgress from './SwapProgress';
import { LiteSend } from './LiteSend';
interface ReduxStateProps {
destinationAddress: string;
@ -19,7 +22,9 @@ interface ReduxStateProps {
reference: string;
secondsRemaining: number | null;
paymentAddress: string | null;
orderStatus: string | null;
provider: string;
bityOrderStatus: string | null;
shapeshiftOrderStatus: string | null;
outputTx: any;
}
@ -27,20 +32,28 @@ interface ReduxActionProps {
restartSwap: TRestartSwap;
startOrderTimerSwap: TStartOrderTimerSwap;
startPollBityOrderStatus: TStartPollBityOrderStatus;
stopOrderTimerSwap: TStopOrderTimerSwap;
stopPollBityOrderStatus: TStopPollBityOrderStatus;
startPollShapeshiftOrderStatus: TStartPollShapeshiftOrderStatus;
stopPollShapeshiftOrderStatus: TStopPollShapeshiftOrderStatus;
stopOrderTimerSwap: TStopOrderTimerSwap;
showNotification: TShowNotification;
}
export default class PartThree extends Component<ReduxActionProps & ReduxStateProps, {}> {
public componentDidMount() {
this.props.startPollBityOrderStatus();
const { provider } = this.props;
if (provider === 'shapeshift') {
this.props.startPollShapeshiftOrderStatus();
} else {
this.props.startPollBityOrderStatus();
}
this.props.startOrderTimerSwap();
}
public componentWillUnmount() {
this.props.stopOrderTimerSwap();
this.props.stopPollBityOrderStatus();
this.props.stopPollShapeshiftOrderStatus();
}
public render() {
@ -49,7 +62,9 @@ export default class PartThree extends Component<ReduxActionProps & ReduxStatePr
origin,
destination,
paymentAddress,
orderStatus,
provider,
bityOrderStatus,
shapeshiftOrderStatus,
destinationAddress,
outputTx,
// ACTIONS
@ -59,7 +74,9 @@ export default class PartThree extends Component<ReduxActionProps & ReduxStatePr
const SwapProgressProps = {
originId: origin.id,
destinationId: destination.id,
orderStatus,
provider,
bityOrderStatus,
shapeshiftOrderStatus,
showNotification,
destinationAddress,
outputTx
@ -72,14 +89,19 @@ export default class PartThree extends Component<ReduxActionProps & ReduxStatePr
const BitcoinQRProps = {
paymentAddress,
destinationAmount: destination.amount
destinationAmount: destination.amount as number
};
const OpenOrder = bityOrderStatus === 'OPEN' || shapeshiftOrderStatus === 'no_deposits';
return (
<div>
<SwapProgress {...SwapProgressProps} />
<PaymentInfo {...PaymentInfoProps} />
{orderStatus === 'OPEN' && origin.id === 'BTC' && <BitcoinQR {...BitcoinQRProps} />}
<LiteSend />
{OpenOrder && origin.id === 'BTC' && <BitcoinQR {...BitcoinQRProps} />}
</div>
);
}

View File

@ -9,14 +9,14 @@
margin: $space auto 0;
max-width: 620px;
width: 100%;
font-size: $font-size-medium-bump;
font-size: $font-size-medium;
text-align: center;
@include mono;
}
@media screen and (max-width: $screen-sm) {
h1 {
font-size: $font-size-medium;
font-size: $font-size-base;
}
}
}

View File

@ -0,0 +1,129 @@
import { NormalizedRates } from 'reducers/swap/types';
import React, { Component } from 'react';
import { toFixedIfLarger } from 'utils/formatters';
import './CurrentRates.scss';
import { ProviderName } from 'actions/swap';
import { objectContainsObjectKeys } from 'utils/helpers';
interface RateInputProps {
rate: number;
amount: number | string;
pair: string;
origin: string;
destination: string;
onChange: any;
}
export const RateInput: React.SFC<RateInputProps> = ({
rate,
amount,
pair,
origin,
destination,
onChange
}) => {
return amount || amount === 0 || amount === '' ? (
<div className="SwapRates-panel-rate">
<input
className="SwapRates-panel-rate-input"
onChange={onChange}
value={amount}
name={pair}
/>
<span className="SwapRates-panel-rate-amount">
{` ${origin} = ${toFixedIfLarger(+amount * rate, 6)} ${destination}`}
</span>
</div>
) : null;
};
interface Props {
provider: ProviderName;
rates: NormalizedRates;
}
interface State {
pairs: { [pair: string]: number };
}
export default class Rates extends Component<Props, State> {
public state = {
pairs: {}
};
public componentDidMount() {
this.setState({ pairs: this.getPairs() });
}
public componentDidUpdate() {
const newPairs = this.getPairs();
// prevents endless loop. if state already contains new pairs, don't set state
if (!objectContainsObjectKeys(newPairs, this.state.pairs)) {
const pairs = {
...this.state.pairs,
...newPairs
};
this.setState({
pairs
});
}
}
public getPairs = () => {
const { rates } = this.props;
const { allIds } = rates;
return allIds.reduce((acc, cur) => {
acc[cur] = 1;
return acc;
}, {});
};
public onChange = (event: any) => {
const { value } = event.target;
const { name } = event.target;
this.setState({
pairs: {
...this.state.pairs,
[name]: value
}
});
};
public buildRateInputs = () => {
const { rates } = this.props;
const { pairs } = this.state;
const fullData: RateInputProps[] = [];
rates.allIds.forEach(each => {
fullData.push({
rate: rates.byId[each].rate,
amount: pairs[each],
pair: each,
origin: rates.byId[each].options[0],
destination: rates.byId[each].options[1],
onChange: this.onChange
});
});
// TODO - don't hardcode only first 4 elements of array.
// not likely to change until significant UI revamp, so not worth spending time on now
return (
<div>
<div className="SwapRates-panel-side col-sm-6">
<RateInput {...fullData[0]} />
<RateInput {...fullData[1]} />
</div>
<div className="SwapRates-panel-side col-sm-6">
<RateInput {...fullData[2]} />
<RateInput {...fullData[3]} />
</div>
</div>
);
};
public render() {
return this.buildRateInputs();
}
}

View File

@ -2,6 +2,7 @@ import {
TBityOrderCreateRequestedSwap,
TChangeStepSwap,
TDestinationAddressSwap,
TShapeshiftOrderCreateRequestedSwap,
TStopLoadBityRatesSwap
} from 'actions/swap';
import { SwapInput } from 'reducers/swap/types';
@ -19,6 +20,8 @@ export interface StateProps {
destinationId: keyof typeof donationAddressMap;
isPostingOrder: boolean;
destinationAddress: string;
destinationKind: number;
provider: string;
}
export interface ActionProps {
@ -26,6 +29,7 @@ export interface ActionProps {
changeStepSwap: TChangeStepSwap;
stopLoadBityRatesSwap: TStopLoadBityRatesSwap;
bityOrderCreateRequestedSwap: TBityOrderCreateRequestedSwap;
shapeshiftOrderCreateRequestedSwap: TShapeshiftOrderCreateRequestedSwap;
}
export default class ReceivingAddress extends Component<StateProps & ActionProps, {}> {
@ -35,15 +39,24 @@ export default class ReceivingAddress extends Component<StateProps & ActionProps
};
public onClickPartTwoComplete = () => {
const { origin, destinationId } = this.props;
const { origin, destinationId, destinationAddress, destinationKind, provider } = this.props;
if (!origin) {
return;
}
this.props.bityOrderCreateRequestedSwap(
origin.amount,
this.props.destinationAddress,
combineAndUpper(origin.id, destinationId)
);
if (provider === 'shapeshift') {
this.props.shapeshiftOrderCreateRequestedSwap(
destinationAddress,
origin.id,
destinationId,
destinationKind
);
} else {
this.props.bityOrderCreateRequestedSwap(
origin.amount as number,
this.props.destinationAddress,
combineAndUpper(origin.id, destinationId)
);
}
};
public render() {
@ -77,7 +90,11 @@ export default class ReceivingAddress extends Component<StateProps & ActionProps
type="text"
value={destinationAddress}
onChange={this.onChangeDestinationAddress}
placeholder={donationAddressMap[destinationId]}
placeholder={
destinationId === 'BTC'
? donationAddressMap[destinationId]
: donationAddressMap.ETH
}
/>
</label>
</div>

View File

@ -0,0 +1,24 @@
.ShapeshiftBanner {
display: block;
width: fit-content;
margin: auto;
margin-top: 16px;
margin-bottom: 16px;
padding: 0px 16px;
background-color: #3a526d;
border-radius: 3px;
box-shadow: 0 3px 8px 0 rgba(0,0,0,0.1), inset 0 0 3px 0 rgba(0,0,0,0.1);
p {
display: inline-block;
color: white;
vertical-align: middle;
margin-bottom: 0px;
}
img {
display: inline-block;
height: 32px;
padding: 8px;
box-sizing: content-box;
margin-left: 16px;
}
}

View File

@ -0,0 +1,12 @@
import React from 'react';
import './ShapeshiftBanner.scss';
import shapeshiftSvg from 'assets/images/logo-shapeshift.svg';
export default () => (
<div className="ShapeshiftBanner">
<p>
<b style={{ paddingRight: '8px' }}>New Feature: </b>Exchange coins & tokens
</p>
<img src={shapeshiftSvg} />
</div>
);

View File

@ -0,0 +1,11 @@
.SupportFooter {
text-align: center;
padding-top: 80px;
&-fallback {
padding: 20px 0;
textarea {
max-width: 35rem;
margin: auto;
}
}
}

View File

@ -0,0 +1,99 @@
import React from 'react';
import './SupportFooter.scss';
import { SwapInput } from 'actions/swap';
import { NormalizedBityRates, NormalizedShapeshiftRates } from 'reducers/swap/types';
interface Props {
origin: SwapInput;
destination: SwapInput;
destinationAddress: string | null;
paymentAddress: string | null;
reference: string | null;
provider: string;
shapeshiftRates: NormalizedShapeshiftRates;
bityRates: NormalizedBityRates;
}
class SupportFooter extends React.Component<Props, {}> {
public state = {
open: false
};
public render() {
const { open } = this.state;
const {
origin,
destination,
destinationAddress,
paymentAddress,
reference,
provider,
shapeshiftRates,
bityRates
} = this.props;
const pair = origin && destination ? origin.id + destination.id : 'BTCETH';
const rates = provider === 'shapeshift' ? shapeshiftRates.byId : bityRates.byId;
const emailTo =
provider === 'shapeshift'
? 'support@myetherwallet.com'
: 'support@myetherwallet.com,mew@bity.com';
const mailSubject = encodeURI('Issue regarding my Swap via MEW');
const serviceProvider = provider.charAt(0).toUpperCase() + provider.slice(1);
let mailBody;
let fallbackBody;
if (pair && rates && rates[pair]) {
mailBody = encodeURI(`Please include the below if this issue is regarding your order.
Provider: ${serviceProvider}
REF ID#: ${reference || ''}
Amount to send: ${origin.amount || ''} ${origin.id}
Amount to receive: ${destination.amount || ''} ${destination.id}
Payment Address: ${paymentAddress || ''}
Receiving Address: ${destinationAddress || ''}
Rate: ${rates[pair].rate} ${origin.id}/${destination.id}
`);
fallbackBody = `To: ${emailTo}
Subject: Issue regarding my Swap via MEW
Message:
Provider: ${serviceProvider}
REF ID#: ${reference || ''}
Amount to send: ${origin.amount || ''} ${origin.id}
Amount to receive: ${destination.amount || ''} ${destination.id}
Payment Address: ${paymentAddress || ''}
Receiving Address: ${destinationAddress || ''}
Rate: ${rates[pair].rate} ${origin.id}/${destination.id}`;
}
return (
<section className="SupportFooter">
<a
className="btn-warning btn-sm"
href={`mailto:${emailTo}?Subject=${mailSubject}&Body=${mailBody}`}
target="_blank"
rel="noopener noreferrer"
>
Issue with your Swap? Contact support
</a>
<div className="SupportFooter-fallback">
<p onClick={this.toggleFallback}>
<small>Click here if link doesn't work</small>
</p>
{open ? (
<textarea defaultValue={fallbackBody} className="form-control input-sm" rows={9} />
) : null}
</div>
</section>
);
}
private toggleFallback = () => {
this.setState({
open: !this.state.open
});
};
}
export default SupportFooter;

View File

@ -75,6 +75,12 @@ $top-height: 40px;
padding: 0 .5rem;
}
&-shapeshift {
overflow: auto;
white-space: normal;
font-size: 1rem;
}
&-label {
font-weight: 100;
}

View File

@ -2,6 +2,7 @@ import { RestartSwapAction } from 'actions/swap';
import { SwapInput } from 'reducers/swap/types';
import React, { Component } from 'react';
import translate from 'translations';
import classnames from 'classnames';
import { toFixedIfLarger } from 'utils/formatters';
import './SwapInfoHeader.scss';
import SwapInfoHeaderTitle from './SwapInfoHeaderTitle';
@ -11,6 +12,7 @@ export interface SwapInfoHeaderProps {
destination: SwapInput;
reference: string;
secondsRemaining: number | null;
provider: string;
restartSwap(): RestartSwapAction;
}
@ -20,7 +22,7 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
if (!origin.amount || !destination.amount) {
return;
}
return destination.amount / origin.amount;
return (destination.amount as number) / (origin.amount as number);
};
public isExpanded = () => {
@ -51,10 +53,18 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
public render() {
const computedOriginDestinationRatio = this.computedOriginDestinationRatio();
const { reference, origin, destination, restartSwap } = this.props;
const { reference, origin, destination, restartSwap, provider } = this.props;
const SwapInfoHeaderTitleProps = {
restartSwap,
provider
};
const referenceClass = classnames(
provider === 'shapeshift' && 'SwapInfo-details-block-shapeshift',
'SwapInfo-details-block-value'
);
return (
<div className="SwapInfo">
<SwapInfoHeaderTitle restartSwap={restartSwap} />
<SwapInfoHeaderTitle {...SwapInfoHeaderTitleProps} />
<section className="SwapInfo-details row">
{/*Amount to send*/}
{!this.isExpanded() && (
@ -67,7 +77,7 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
{/*Reference Number*/}
{this.isExpanded() && (
<div className={this.computedClass()}>
<h3 className="SwapInfo-details-block-value">{reference}</h3>
<h3 className={referenceClass}>{reference}</h3>
<p className="SwapInfo-details-block-label">{translate('SWAP_ref_num')}</p>
</div>
)}
@ -83,7 +93,7 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
{/*Amount to Receive*/}
<div className={this.computedClass()}>
<h3 className="SwapInfo-details-block-value">
{` ${destination.amount} ${destination.id}`}
{` ${toFixedIfLarger(destination.amount as number, 4)} ${destination.id}`}
</h3>
<p className="SwapInfo-details-block-label">{translate('SWAP_rec_amt')}</p>
</div>
@ -92,7 +102,7 @@ export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
<div className={this.computedClass()}>
<h3 className="SwapInfo-details-block-value">
{`${computedOriginDestinationRatio &&
toFixedIfLarger(computedOriginDestinationRatio)} ${destination.id}/${origin.id}`}
toFixedIfLarger(computedOriginDestinationRatio, 4)} ${destination.id}/${origin.id}`}
</h3>
<p className="SwapInfo-details-block-label">{translate('SWAP_your_rate')}</p>
</div>

View File

@ -1,16 +1,20 @@
import { RestartSwapAction } from 'actions/swap';
import bityLogo from 'assets/images/logo-bity.svg';
import shapeshiftLogo from 'assets/images/shapeshift-dark.svg';
import { bityReferralURL } from 'config/data';
import React, { Component } from 'react';
import translate from 'translations';
import './SwapInfoHeader.scss';
export interface SwapInfoHeaderTitleProps {
provider: string;
restartSwap(): RestartSwapAction;
}
export default class SwapInfoHeaderTitle extends Component<SwapInfoHeaderTitleProps, {}> {
public render() {
const { provider } = this.props;
const logoToRender = provider === 'shapeshift' ? shapeshiftLogo : bityLogo;
return (
<section className="SwapInfo-top row text-center">
<div className="col-xs-3 text-left">
@ -24,7 +28,7 @@ export default class SwapInfoHeaderTitle extends Component<SwapInfoHeaderTitlePr
</div>
<div className="col-xs-3">
<a className="SwapInfo-top-logo" href={bityReferralURL} target="_blank" rel="noopener">
<img className="SwapInfo-top-logo-img" src={bityLogo} />
<img className="SwapInfo-top-logo-img" src={logoToRender} />
</a>
</div>
</section>

View File

@ -9,7 +9,9 @@ export interface Props {
originId: string;
destinationAddress: string;
outputTx: string;
orderStatus: string | null;
provider: string;
bityOrderStatus: string | null;
shapeshiftOrderStatus: string | null;
// actions
showNotification: TShowNotification;
}
@ -28,9 +30,17 @@ export default class SwapProgress extends Component<Props, State> {
public showSwapNotification = () => {
const { hasShownViewTx } = this.state;
const { destinationId, outputTx, showNotification, orderStatus } = this.props;
const {
destinationId,
outputTx,
showNotification,
provider,
bityOrderStatus,
shapeshiftOrderStatus
} = this.props;
const isShapeshift = provider === 'shapeshift';
if (orderStatus === 'FILL') {
if (isShapeshift ? shapeshiftOrderStatus === 'complete' : bityOrderStatus === 'FILL') {
if (!hasShownViewTx) {
let linkElement: React.ReactElement<HTMLAnchorElement>;
let link;
@ -40,7 +50,7 @@ export default class SwapProgress extends Component<Props, State> {
link = bityConfig.ETHTxExplorer(outputTx);
linkElement = (
<a href={link} target="_blank" rel="noopener">
${notificationMessage}
{notificationMessage}
</a>
);
// BTC uses a different explorer
@ -48,7 +58,7 @@ export default class SwapProgress extends Component<Props, State> {
link = bityConfig.BTCTxExplorer(outputTx);
linkElement = (
<a href={link} target="_blank" rel="noopener">
${notificationMessage}
{notificationMessage}
</a>
);
}
@ -60,11 +70,12 @@ export default class SwapProgress extends Component<Props, State> {
};
public computedClass = (step: number) => {
const { orderStatus } = this.props;
const { bityOrderStatus, shapeshiftOrderStatus } = this.props;
let cssClass = 'SwapProgress-item';
const orderStatus = bityOrderStatus || shapeshiftOrderStatus;
switch (orderStatus) {
case 'no_deposits':
case 'OPEN':
if (step < 2) {
return cssClass + ' is-complete';
@ -73,6 +84,7 @@ export default class SwapProgress extends Component<Props, State> {
} else {
return cssClass;
}
case 'received':
case 'RCVE':
if (step < 4) {
return cssClass + ' is-complete';
@ -81,9 +93,11 @@ export default class SwapProgress extends Component<Props, State> {
} else {
return cssClass;
}
case 'complete':
case 'FILL':
cssClass += ' is-complete';
return cssClass;
case 'failed':
case 'CANC':
return cssClass;
default:

View File

@ -2,91 +2,128 @@ import { showNotification as dShowNotification, TShowNotification } from 'action
import {
initSwap as dInitSwap,
bityOrderCreateRequestedSwap as dBityOrderCreateRequestedSwap,
shapeshiftOrderCreateRequestedSwap as dShapeshiftOrderCreateRequestedSwap,
changeStepSwap as dChangeStepSwap,
destinationAddressSwap as dDestinationAddressSwap,
loadBityRatesRequestedSwap as dLoadBityRatesRequestedSwap,
loadShapeshiftRatesRequestedSwap as dLoadShapeshiftRatesRequestedSwap,
restartSwap as dRestartSwap,
startOrderTimerSwap as dStartOrderTimerSwap,
startPollBityOrderStatus as dStartPollBityOrderStatus,
startPollShapeshiftOrderStatus as dStartPollShapeshiftOrderStatus,
stopLoadBityRatesSwap as dStopLoadBityRatesSwap,
stopLoadShapeshiftRatesSwap as dStopLoadShapeshiftRatesSwap,
stopOrderTimerSwap as dStopOrderTimerSwap,
stopPollBityOrderStatus as dStopPollBityOrderStatus,
stopPollShapeshiftOrderStatus as dStopPollShapeshiftOrderStatus,
changeSwapProvider as dChangeSwapProvider,
TInitSwap,
TBityOrderCreateRequestedSwap,
TChangeStepSwap,
TDestinationAddressSwap,
TLoadBityRatesRequestedSwap,
TShapeshiftOrderCreateRequestedSwap,
TLoadShapeshiftRequestedSwap,
TRestartSwap,
TStartOrderTimerSwap,
TStartPollBityOrderStatus,
TStartPollShapeshiftOrderStatus,
TStopLoadBityRatesSwap,
TStopOrderTimerSwap,
TStopPollBityOrderStatus
TStopPollBityOrderStatus,
TStopPollShapeshiftOrderStatus,
TChangeSwapProvider,
TStopLoadShapeshiftRatesSwap,
ProviderName
} from 'actions/swap';
import { SwapInput, NormalizedOptions, NormalizedBityRates } from 'reducers/swap/types';
import {
SwapInput,
NormalizedOptions,
NormalizedBityRates,
NormalizedShapeshiftRates
} from 'reducers/swap/types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import CurrencySwap from './components/CurrencySwap';
import CurrentRates from './components/CurrentRates';
import PartThree from './components/PartThree';
import SupportFooter from './components/SupportFooter';
import ReceivingAddress from './components/ReceivingAddress';
import SwapInfoHeader from './components/SwapInfoHeader';
import ShapeshiftBanner from './components/ShapeshiftBanner';
import TabSection from 'containers/TabSection';
import { merge } from 'lodash';
interface ReduxStateProps {
step: number;
origin: SwapInput;
destination: SwapInput;
bityRates: NormalizedBityRates;
shapeshiftRates: NormalizedShapeshiftRates;
options: NormalizedOptions;
provider: ProviderName;
bityOrder: any;
shapeshiftOrder: any;
destinationAddress: string;
isFetchingRates: boolean | null;
secondsRemaining: number | null;
outputTx: string | null;
isPostingOrder: boolean;
orderStatus: string | null;
bityOrderStatus: string | null;
shapeshiftOrderStatus: string | null;
paymentAddress: string | null;
}
interface ReduxActionProps {
changeStepSwap: TChangeStepSwap;
loadBityRatesRequestedSwap: TLoadBityRatesRequestedSwap;
loadShapeshiftRatesRequestedSwap: TLoadShapeshiftRequestedSwap;
destinationAddressSwap: TDestinationAddressSwap;
restartSwap: TRestartSwap;
stopLoadBityRatesSwap: TStopLoadBityRatesSwap;
stopLoadShapeshiftRatesSwap: TStopLoadShapeshiftRatesSwap;
shapeshiftOrderCreateRequestedSwap: TShapeshiftOrderCreateRequestedSwap;
bityOrderCreateRequestedSwap: TBityOrderCreateRequestedSwap;
startPollShapeshiftOrderStatus: TStartPollShapeshiftOrderStatus;
startPollBityOrderStatus: TStartPollBityOrderStatus;
startOrderTimerSwap: TStartOrderTimerSwap;
stopOrderTimerSwap: TStopOrderTimerSwap;
stopPollBityOrderStatus: TStopPollBityOrderStatus;
stopPollShapeshiftOrderStatus: TStopPollShapeshiftOrderStatus;
showNotification: TShowNotification;
startOrderTimerSwap: TStartOrderTimerSwap;
initSwap: TInitSwap;
swapProvider: TChangeSwapProvider;
}
class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
public componentDidMount() {
this.props.loadBityRatesRequestedSwap();
this.props.loadShapeshiftRatesRequestedSwap();
}
public componentWillUnmount() {
this.props.stopLoadBityRatesSwap();
this.props.stopLoadShapeshiftRatesSwap();
}
public render() {
const {
// STATE
bityRates,
shapeshiftRates,
provider,
options,
origin,
destination,
destinationAddress,
step,
bityOrder,
shapeshiftOrder,
secondsRemaining,
paymentAddress,
orderStatus,
bityOrderStatus,
shapeshiftOrderStatus,
isPostingOrder,
outputTx,
// ACTIONS
@ -96,24 +133,31 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
changeStepSwap,
destinationAddressSwap,
bityOrderCreateRequestedSwap,
shapeshiftOrderCreateRequestedSwap,
showNotification,
startOrderTimerSwap,
startPollBityOrderStatus,
stopPollShapeshiftOrderStatus,
startPollShapeshiftOrderStatus,
stopOrderTimerSwap,
stopPollBityOrderStatus
stopPollBityOrderStatus,
swapProvider
} = this.props;
const { reference } = bityOrder;
const reference = provider === 'shapeshift' ? shapeshiftOrder.orderId : bityOrder.reference;
const ReceivingAddressProps = {
isPostingOrder,
origin,
destinationId: destination.id,
destinationKind: destination.amount as number,
destinationAddressSwap,
destinationAddress,
stopLoadBityRatesSwap,
changeStepSwap,
bityOrderCreateRequestedSwap
provider,
bityOrderCreateRequestedSwap,
shapeshiftOrderCreateRequestedSwap
};
const SwapInfoHeaderProps = {
@ -122,19 +166,29 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
reference,
secondsRemaining,
restartSwap,
orderStatus
bityOrderStatus,
shapeshiftOrderStatus,
provider
};
const CurrencySwapProps = {
showNotification,
bityRates,
shapeshiftRates,
provider,
options,
initSwap,
swapProvider,
changeStepSwap
};
const paymentInfo =
provider === 'shapeshift'
? merge(origin, { amount: shapeshiftOrder.depositAmount })
: merge(origin, { amount: bityOrder.amount });
const PaymentInfoProps = {
origin,
origin: paymentInfo,
paymentAddress
};
@ -142,29 +196,44 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
...SwapInfoHeaderProps,
...PaymentInfoProps,
reference,
provider,
startOrderTimerSwap,
startPollBityOrderStatus,
stopOrderTimerSwap,
startPollBityOrderStatus,
startPollShapeshiftOrderStatus,
stopPollBityOrderStatus,
stopPollShapeshiftOrderStatus,
showNotification,
destinationAddress,
outputTx
};
const CurrentRatesProps = bityRates.byId ? { ...bityRates.byId } : {};
const SupportProps = {
origin,
destination,
destinationAddress,
paymentAddress,
reference,
provider,
shapeshiftRates,
bityRates
};
const CurrentRatesProps = { provider, bityRates, shapeshiftRates };
return (
<TabSection>
<section className="Tab-content swap-tab">
{step === 1 && <CurrentRates {...CurrentRatesProps} />}
{step === 1 && <ShapeshiftBanner />}
{(step === 2 || step === 3) && <SwapInfoHeader {...SwapInfoHeaderProps} />}
<main className="Tab-content-pane">
{step === 1 && <CurrencySwap {...CurrencySwapProps} />}
{step === 2 && <ReceivingAddress {...ReceivingAddressProps} />}
{step === 3 && <PartThree {...PartThreeProps} />}
</main>
</section>
<SupportFooter {...SupportProps} />
</TabSection>
);
}
@ -176,14 +245,18 @@ function mapStateToProps(state: AppState) {
origin: state.swap.origin,
destination: state.swap.destination,
bityRates: state.swap.bityRates,
shapeshiftRates: state.swap.shapeshiftRates,
provider: state.swap.provider,
options: state.swap.options,
bityOrder: state.swap.bityOrder,
shapeshiftOrder: state.swap.shapeshiftOrder,
destinationAddress: state.swap.destinationAddress,
isFetchingRates: state.swap.isFetchingRates,
secondsRemaining: state.swap.secondsRemaining,
outputTx: state.swap.outputTx,
isPostingOrder: state.swap.isPostingOrder,
orderStatus: state.swap.orderStatus,
bityOrderStatus: state.swap.bityOrderStatus,
shapeshiftOrderStatus: state.swap.shapeshiftOrderStatus,
paymentAddress: state.swap.paymentAddress
};
}
@ -192,13 +265,19 @@ export default connect(mapStateToProps, {
changeStepSwap: dChangeStepSwap,
initSwap: dInitSwap,
bityOrderCreateRequestedSwap: dBityOrderCreateRequestedSwap,
shapeshiftOrderCreateRequestedSwap: dShapeshiftOrderCreateRequestedSwap,
loadBityRatesRequestedSwap: dLoadBityRatesRequestedSwap,
loadShapeshiftRatesRequestedSwap: dLoadShapeshiftRatesRequestedSwap,
destinationAddressSwap: dDestinationAddressSwap,
restartSwap: dRestartSwap,
startOrderTimerSwap: dStartOrderTimerSwap,
startPollBityOrderStatus: dStartPollBityOrderStatus,
startPollShapeshiftOrderStatus: dStartPollShapeshiftOrderStatus,
stopLoadBityRatesSwap: dStopLoadBityRatesSwap,
stopLoadShapeshiftRatesSwap: dStopLoadShapeshiftRatesSwap,
stopOrderTimerSwap: dStopOrderTimerSwap,
stopPollBityOrderStatus: dStopPollBityOrderStatus,
showNotification: dShowNotification
stopPollShapeshiftOrderStatus: dStopPollShapeshiftOrderStatus,
showNotification: dShowNotification,
swapProvider: dChangeSwapProvider
})(Swap);

View File

@ -2,15 +2,7 @@ import { Wei } from 'libs/units';
import * as eth from './ether';
import { IFullWallet } from 'libs/wallet';
import { ITransaction } from '../typings';
export {
enoughBalanceViaTx,
validateTx,
validGasLimit,
makeTransaction,
getTransactionFields,
computeIndexingHash
} from './ether';
export * from './token';
export const signTransaction = async (
t: ITransaction,
w: IFullWallet,
@ -21,3 +13,13 @@ export const signTransaction = async (
const signedT = await eth.signTx(t, w);
return signedT;
};
export {
enoughBalanceViaTx,
validateTx,
validGasLimit,
makeTransaction,
getTransactionFields,
computeIndexingHash
} from './ether';
export * from './token';

View File

@ -103,7 +103,7 @@ const fromTokenBase = (value: TokenValue, decimal: number) =>
const toTokenBase = (value: string, decimal: number) =>
TokenValue(convertedToBaseUnit(value, decimal));
const isEtherUnit = (unit: string) => unit === 'ether';
const isEtherUnit = (unit: string) => unit === 'ether' || unit === 'ETH';
const convertTokenBase = (value: TokenValue, oldDecimal: number, newDecimal: number) => {
if (oldDecimal === newDecimal) {

View File

@ -10,17 +10,23 @@ export interface State {
destination: stateTypes.SwapInput;
options: stateTypes.NormalizedOptions;
bityRates: stateTypes.NormalizedBityRates;
// Change this
shapeshiftRates: stateTypes.NormalizedBityRates;
provider: string;
bityOrder: any;
shapeshiftOrder: any;
destinationAddress: string;
isFetchingRates: boolean | null;
secondsRemaining: number | null;
outputTx: string | null;
isPostingOrder: boolean;
orderStatus: string | null;
bityOrderStatus: string | null;
shapeshiftOrderStatus: string | null;
orderTimestampCreatedISOString: string | null;
paymentAddress: string | null;
validFor: number | null;
orderId: string | null;
showLiteSend: boolean;
}
export const INITIAL_STATE: State = {
@ -35,17 +41,25 @@ export const INITIAL_STATE: State = {
byId: {},
allIds: []
},
shapeshiftRates: {
byId: {},
allIds: []
},
provider: 'bity',
destinationAddress: '',
bityOrder: {},
shapeshiftOrder: {},
isFetchingRates: null,
secondsRemaining: null,
outputTx: null,
isPostingOrder: false,
orderStatus: null,
bityOrderStatus: null,
shapeshiftOrderStatus: null,
orderTimestampCreatedISOString: null,
paymentAddress: null,
validFor: null,
orderId: null
orderId: null,
showLiteSend: false
};
export function swap(state: State = INITIAL_STATE, action: actionTypes.SwapAction) {
@ -55,12 +69,41 @@ export function swap(state: State = INITIAL_STATE, action: actionTypes.SwapActio
return {
...state,
bityRates: {
byId: normalize(payload, [schema.bityRate]).entities.bityRates,
allIds: schema.allIds(normalize(payload, [schema.bityRate]).entities.bityRates)
byId: normalize(payload, [schema.providerRate]).entities.providerRates,
allIds: schema.allIds(normalize(payload, [schema.providerRate]).entities.providerRates)
},
options: {
byId: normalize(payload, [schema.bityRate]).entities.options,
allIds: schema.allIds(normalize(payload, [schema.bityRate]).entities.options)
byId: Object.assign(
{},
normalize(payload, [schema.providerRate]).entities.options,
state.options.byId
),
allIds: [
...schema.allIds(normalize(payload, [schema.providerRate]).entities.options),
...state.options.allIds
]
},
isFetchingRates: false
};
case TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED:
return {
...state,
shapeshiftRates: {
byId: normalize(action.payload, [schema.providerRate]).entities.providerRates,
allIds: schema.allIds(
normalize(action.payload, [schema.providerRate]).entities.providerRates
)
},
options: {
byId: Object.assign(
{},
normalize(action.payload, [schema.providerRate]).entities.options,
state.options.byId
),
allIds: [
...schema.allIds(normalize(action.payload, [schema.providerRate]).entities.options),
...state.options.allIds
]
},
isFetchingRates: false
};
@ -85,19 +128,30 @@ export function swap(state: State = INITIAL_STATE, action: actionTypes.SwapActio
case TypeKeys.SWAP_RESTART:
return {
...INITIAL_STATE,
bityRates: state.bityRates
options: state.options,
bityRates: state.bityRates,
shapeshiftRates: state.shapeshiftRates
};
case TypeKeys.SWAP_ORDER_CREATE_REQUESTED:
case TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED:
return {
...state,
isPostingOrder: true
};
case TypeKeys.SWAP_ORDER_CREATE_FAILED:
case TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED:
return {
...state,
isPostingOrder: true
};
case TypeKeys.SWAP_BITY_ORDER_CREATE_FAILED:
return {
...state,
isPostingOrder: false
};
case TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_FAILED:
return {
...state,
isPostingOrder: false
};
// TODO - fix bad naming
case TypeKeys.SWAP_BITY_ORDER_CREATE_SUCCEEDED:
return {
...state,
@ -111,18 +165,43 @@ export function swap(state: State = INITIAL_STATE, action: actionTypes.SwapActio
validFor: action.payload.validFor, // to build from local storage
orderTimestampCreatedISOString: action.payload.timestamp_created,
paymentAddress: action.payload.payment_address,
orderStatus: action.payload.status,
bityOrderStatus: action.payload.status,
orderId: action.payload.id
};
case TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED:
const currDate = Date.now();
const secondsRemaining = Math.floor((+new Date(action.payload.expiration) - currDate) / 1000);
return {
...state,
shapeshiftOrder: {
...action.payload
},
isPostingOrder: false,
originAmount: parseFloat(action.payload.depositAmount),
destinationAmount: parseFloat(action.payload.withdrawalAmount),
secondsRemaining,
validFor: secondsRemaining,
orderTimestampCreatedISOString: new Date(currDate).toISOString(),
paymentAddress: action.payload.deposit,
shapeshiftOrderStatus: 'no_deposits',
orderId: action.payload.orderId
};
case TypeKeys.SWAP_BITY_ORDER_STATUS_SUCCEEDED:
return {
...state,
outputTx: action.payload.output.reference,
orderStatus:
bityOrderStatus:
action.payload.output.status === 'FILL'
? action.payload.output.status
: action.payload.input.status
};
case TypeKeys.SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED:
return {
...state,
outputTx: action.payload && action.payload.transaction ? action.payload.transaction : null,
shapeshiftOrderStatus: action.payload.status
};
case TypeKeys.SWAP_ORDER_TIME:
return {
...state,
@ -134,13 +213,31 @@ export function swap(state: State = INITIAL_STATE, action: actionTypes.SwapActio
...state,
isFetchingRates: true
};
case TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED:
return {
...state,
isFetchingRates: true
};
case TypeKeys.SWAP_STOP_LOAD_BITY_RATES:
return {
...state,
isFetchingRates: false
};
case TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES:
return {
...state,
isFetchingRates: false
};
case TypeKeys.SWAP_CHANGE_PROVIDER:
return {
...state,
provider: action.payload
};
case TypeKeys.SWAP_SHOW_LITE_SEND:
return {
...state,
showLiteSend: action.payload
};
default:
return state;
}

View File

@ -3,7 +3,8 @@ import { schema } from 'normalizr';
export const allIds = (byIds: { [name: string]: {} }) => {
return Object.keys(byIds);
};
export const option = new schema.Entity('options');
export const bityRate = new schema.Entity('bityRates', {
export const providerRate = new schema.Entity('providerRates', {
options: [option]
});

View File

@ -3,10 +3,21 @@ import { WhitelistedCoins } from 'config/bity';
export interface SwapInput {
id: WhitelistedCoins;
amount: number;
amount: number | string;
}
export interface NormalizedBityRate {
export interface NormalizedRate {
id: number;
options: WhitelistedCoins[];
rate: number;
}
export interface NormalizedRates {
byId: { [id: string]: NormalizedRate };
allIds: string[];
}
export interface NormalizedBityRate extends NormalizedRate {
id: number;
options: WhitelistedCoins[];
rate: number;
@ -17,6 +28,19 @@ export interface NormalizedBityRates {
allIds: string[];
}
export interface NormalizedShapeshiftRate extends NormalizedRate {
id: number;
options: WhitelistedCoins[];
rate: number;
limit: number;
min: number;
}
export interface NormalizedShapeshiftRates {
byId: { [id: string]: NormalizedShapeshiftRate };
allIds: string[];
}
export interface NormalizedOptions {
byId: { [id: string]: Option };
allIds: string[];

View File

@ -4,7 +4,8 @@ import {
SetWalletAction,
WalletAction,
SetWalletConfigAction,
TypeKeys
TypeKeys,
SetTokenBalanceFulfilledAction
} from 'actions/wallet';
import { TokenValue } from 'libs/units';
import { IWallet, Balance, WalletConfig } from 'libs/wallet';
@ -69,6 +70,30 @@ function setTokenBalancesPending(state: State): State {
};
}
function setTokenBalancePending(state: State): State {
return {
...state,
isTokensLoading: true,
tokensError: null
};
}
function setTokenBalanceFufilled(state: State, action: SetTokenBalanceFulfilledAction): State {
return {
...state,
tokens: { ...state.tokens, ...action.payload },
isTokensLoading: false
};
}
function setTokenBalanceRejected(state: State): State {
return {
...state,
isTokensLoading: false,
tokensError: 'Failed to fetch token value'
};
}
function setTokenBalancesFulfilled(state: State, action: SetTokenBalancesFulfilledAction): State {
return {
...state,
@ -124,6 +149,12 @@ export function wallet(state: State = INITIAL_STATE, action: WalletAction): Stat
return setTokenBalancesFulfilled(state, action);
case TypeKeys.WALLET_SET_TOKEN_BALANCES_REJECTED:
return setTokenBalancesRejected(state);
case TypeKeys.WALLET_SET_TOKEN_BALANCE_PENDING:
return setTokenBalancePending(state);
case TypeKeys.WALLET_SET_TOKEN_BALANCE_FULFILLED:
return setTokenBalanceFufilled(state, action);
case TypeKeys.WALLET_SET_TOKEN_BALANCE_REJECTED:
return setTokenBalanceRejected(state);
case TypeKeys.WALLET_SCAN_WALLET_FOR_TOKENS:
return scanWalletForTokens(state);
case TypeKeys.WALLET_SET_WALLET_TOKENS:

View File

@ -1,18 +1,33 @@
import configSaga from './config';
import deterministicWallets from './deterministicWallets';
import notifications from './notifications';
import { bityTimeRemaining, pollBityOrderStatusSaga, postBityOrderSaga } from './swap/orders';
import { getBityRatesSaga } from './swap/rates';
import {
swapTimerSaga,
pollBityOrderStatusSaga,
postBityOrderSaga,
postShapeshiftOrderSaga,
pollShapeshiftOrderStatusSaga,
restartSwapSaga
} from './swap/orders';
import { liteSend } from './swap/liteSend';
import { getBityRatesSaga, getShapeShiftRatesSaga, swapProviderSaga } from './swap/rates';
import wallet from './wallet';
import { transaction } from './transaction';
export default {
transaction,
bityTimeRemaining,
liteSend,
configSaga,
postBityOrderSaga,
postShapeshiftOrderSaga,
pollBityOrderStatusSaga,
pollShapeshiftOrderStatusSaga,
getBityRatesSaga,
getShapeShiftRatesSaga,
swapTimerSaga,
restartSwapSaga,
notifications,
wallet,
deterministicWallets
transaction,
deterministicWallets,
swapProviderSaga
};

View File

@ -0,0 +1,109 @@
import { SagaIterator, delay } from 'redux-saga';
import { select, put, call, take, race, fork, cancel, takeEvery } from 'redux-saga/effects';
import { getOrigin, getPaymentAddress } from 'selectors/swap';
import {
setUnitMeta,
setCurrentTo,
setCurrentValue,
TypeKeys as TransactionTK,
reset
} from 'actions/transaction';
import { TypeKeys as WalletTK, setTokenBalancePending } from 'actions/wallet';
import { AppState } from 'reducers';
import { showNotification } from 'actions/notifications';
import { isSupportedUnit } from 'selectors/config';
import { isEtherUnit } from 'libs/units';
import { showLiteSend, configureLiteSend } from 'actions/swap';
import { TypeKeys as SwapTK } from 'actions/swap/constants';
import { isUnlocked } from 'selectors/wallet';
type SwapState = AppState['swap'];
export function* configureLiteSendSaga(): SagaIterator {
const { amount, id }: SwapState['origin'] = yield select(getOrigin);
const paymentAddress: SwapState['paymentAddress'] = yield call(fetchPaymentAddress);
if (!paymentAddress) {
yield put(showNotification('danger', 'Could not fetch payment address'));
return yield put(showLiteSend(false));
}
const supportedUnit: boolean = yield select(isSupportedUnit, id);
if (!supportedUnit) {
return yield put(showLiteSend(false));
}
const unlocked: boolean = yield select(isUnlocked);
yield put(showLiteSend(true));
// wait for wallet to be unlocked to continue
if (!unlocked) {
yield take(WalletTK.WALLET_SET);
}
//if it's a token, manually scan for that tokens balance and wait for it to resolve
if (!isEtherUnit(id)) {
yield put(setTokenBalancePending({ tokenSymbol: id }));
yield take([
WalletTK.WALLET_SET_TOKEN_BALANCE_FULFILLED,
WalletTK.WALLET_SET_TOKEN_BALANCE_REJECTED
]);
}
yield put(setUnitMeta(id));
yield put(setCurrentValue(amount.toString()));
yield put(setCurrentTo(paymentAddress));
}
export function* handleConfigureLiteSend(): SagaIterator {
while (true) {
const liteSendProc = yield fork(configureLiteSendSaga);
const result = yield race({
transactionReset: take(TransactionTK.RESET),
userNavigatedAway: take(WalletTK.WALLET_RESET),
bityPollingFinished: take(SwapTK.SWAP_STOP_POLL_BITY_ORDER_STATUS),
shapeshiftPollingFinished: take(SwapTK.SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS)
});
//if polling is finished we should clear state and hide this tab
if (result.bityPollingFinished || result.shapeshiftPollingFinished) {
//clear transaction state and cancel saga
yield cancel(liteSendProc);
yield put(showLiteSend(false));
return yield put(reset());
}
if (result.transactionReset) {
yield cancel(liteSendProc);
}
// if wallet reset is called, that means the user navigated away from the page, so we cancel everything
if (result.userNavigatedAway) {
yield cancel(liteSendProc);
yield put(showLiteSend(false));
return yield put(configureLiteSend());
}
// else the user just swapped to a new wallet, and we'll race against liteSend again to re-apply
// the same transaction parameters again
}
}
export function* fetchPaymentAddress(): SagaIterator {
const MAX_RETRIES = 5;
let currentTry = 0;
while (currentTry <= MAX_RETRIES) {
yield call(delay, 500);
const paymentAddress: SwapState['paymentAddress'] = yield select(getPaymentAddress);
if (paymentAddress) {
return paymentAddress;
}
currentTry++;
}
yield put(showNotification('danger', 'Payment address not found'));
return false;
}
export function* liteSend(): SagaIterator {
yield takeEvery(SwapTK.SWAP_CONFIGURE_LITE_SEND, handleConfigureLiteSend);
}

View File

@ -4,41 +4,72 @@ import {
BityOrderCreateRequestedSwapAction,
bityOrderCreateSucceededSwap,
changeStepSwap,
orderStatusRequestedSwap,
orderStatusSucceededSwap,
orderTimeSwap,
startOrderTimerSwap,
startPollBityOrderStatus,
stopLoadBityRatesSwap,
stopPollBityOrderStatus
stopPollBityOrderStatus,
shapeshiftOrderStatusSucceededSwap,
ShapeshiftOrderCreateRequestedSwapAction,
stopLoadShapeshiftRatesSwap,
shapeshiftOrderCreateFailedSwap,
shapeshiftOrderCreateSucceededSwap,
startPollShapeshiftOrderStatus,
stopPollShapeshiftOrderStatus,
bityOrderStatusRequested,
stopOrderTimerSwap,
bityOrderStatusSucceededSwap,
shapeshiftOrderStatusRequested,
loadShapeshiftRatesRequestedSwap
} from 'actions/swap';
import { getOrderStatus, postOrder } from 'api/bity';
import moment from 'moment';
import { AppState } from 'reducers';
import { State as SwapState } from 'reducers/swap';
import { delay, SagaIterator } from 'redux-saga';
import { call, cancel, cancelled, fork, put, select, take, takeEvery } from 'redux-saga/effects';
import {
call,
cancel,
apply,
cancelled,
fork,
put,
select,
take,
takeEvery
} from 'redux-saga/effects';
import shapeshift from 'api/shapeshift';
import { TypeKeys } from 'actions/swap/constants';
import { resetWallet } from 'actions/wallet';
import { reset } from 'actions/transaction';
export const getSwap = (state: AppState): SwapState => state.swap;
const ONE_SECOND = 1000;
const TEN_SECONDS = ONE_SECOND * 10;
export const BITY_TIMEOUT_MESSAGE = `
export const ORDER_TIMEOUT_MESSAGE = `
Time has run out.
If you have already sent, please wait 1 hour.
If your order has not be processed after 1 hour,
please press the orange 'Issue with your Swap?' button.
`;
export const ORDER_RECEIVED_MESSAGE = `
The order was recieved.
It may take some time to process the transaction.
Please wait 1 hour. If your order has not been processed by then,
please press the orange 'Issue with your Swap?' button.
`;
export function* pollBityOrderStatus(): SagaIterator {
try {
let swap = yield select(getSwap);
while (true) {
yield put(orderStatusRequestedSwap());
yield put(bityOrderStatusRequested());
const orderStatus = yield call(getOrderStatus, swap.orderId);
if (orderStatus.error) {
yield put(showNotification('danger', `Bity Error: ${orderStatus.msg}`, TEN_SECONDS));
} else {
yield put(orderStatusSucceededSwap(orderStatus.data));
yield put(bityOrderStatusSucceededSwap(orderStatus.data));
yield call(delay, ONE_SECOND * 5);
swap = yield select(getSwap);
if (swap === 'CANC') {
@ -54,18 +85,51 @@ export function* pollBityOrderStatus(): SagaIterator {
}
}
export function* pollShapeshiftOrderStatus(): SagaIterator {
try {
let swap = yield select(getSwap);
while (true) {
yield put(shapeshiftOrderStatusRequested());
const orderStatus = yield apply(shapeshift, shapeshift.checkStatus, [swap.paymentAddress]);
if (orderStatus.status === 'failed') {
yield put(showNotification('danger', `Shapeshift Error: ${orderStatus.error}`, Infinity));
yield put(stopPollShapeshiftOrderStatus());
} else {
yield put(shapeshiftOrderStatusSucceededSwap(orderStatus));
yield call(delay, ONE_SECOND * 5);
swap = yield select(getSwap);
if (swap === 'CANC') {
break;
}
}
}
} finally {
if (yield cancelled()) {
// Request canclled
}
}
}
export function* pollBityOrderStatusSaga(): SagaIterator {
while (yield take('SWAP_START_POLL_BITY_ORDER_STATUS')) {
while (yield take(TypeKeys.SWAP_START_POLL_BITY_ORDER_STATUS)) {
// starts the task in the background
const pollBityOrderStatusTask = yield fork(pollBityOrderStatus);
// wait for the user to get to point where refresh is no longer needed
yield take('SWAP_STOP_POLL_BITY_ORDER_STATUS');
yield take(TypeKeys.SWAP_STOP_POLL_BITY_ORDER_STATUS);
// cancel the background task
// this will cause the forked loadBityRates task to jump into its finally block
yield cancel(pollBityOrderStatusTask);
}
}
export function* pollShapeshiftOrderStatusSaga(): SagaIterator {
while (yield take(TypeKeys.SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS)) {
const pollShapeshiftOrderStatusTask = yield fork(pollShapeshiftOrderStatus);
yield take(TypeKeys.SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS);
yield cancel(pollShapeshiftOrderStatusTask);
}
}
export function* postBityOrderCreate(action: BityOrderCreateRequestedSwapAction): SagaIterator {
const payload = action.payload;
try {
@ -97,17 +161,63 @@ export function* postBityOrderCreate(action: BityOrderCreateRequestedSwapAction)
}
}
export function* postBityOrderSaga(): SagaIterator {
yield takeEvery('SWAP_ORDER_CREATE_REQUESTED', postBityOrderCreate);
export function* postShapeshiftOrderCreate(
action: ShapeshiftOrderCreateRequestedSwapAction
): SagaIterator {
const payload = action.payload;
try {
yield put(stopLoadShapeshiftRatesSwap());
const order = yield apply(shapeshift, shapeshift.sendAmount, [
payload.withdrawal,
payload.originKind,
payload.destinationKind,
payload.destinationAmount
]);
if (order.error) {
yield put(showNotification('danger', `Shapeshift Error: ${order.error}`, TEN_SECONDS));
yield put(shapeshiftOrderCreateFailedSwap());
} else {
yield put(shapeshiftOrderCreateSucceededSwap(order.success));
yield put(changeStepSwap(3));
// start countdown
yield put(startOrderTimerSwap());
// start shapeshift order status polling
yield put(startPollShapeshiftOrderStatus());
}
} catch (e) {
const message =
'Connection Error. Please check the developer console for more details and/or contact support';
yield put(showNotification('danger', message, TEN_SECONDS));
yield put(shapeshiftOrderCreateFailedSwap());
}
}
export function* bityTimeRemaining(): SagaIterator {
while (yield take('SWAP_ORDER_START_TIMER')) {
export function* postBityOrderSaga(): SagaIterator {
yield takeEvery(TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED, postBityOrderCreate);
}
export function* postShapeshiftOrderSaga(): SagaIterator {
yield takeEvery(TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED, postShapeshiftOrderCreate);
}
export function* restartSwap() {
yield put(reset());
yield put(resetWallet());
yield put(stopPollShapeshiftOrderStatus());
yield put(stopPollBityOrderStatus());
yield put(loadShapeshiftRatesRequestedSwap());
}
export function* restartSwapSaga(): SagaIterator {
yield takeEvery(TypeKeys.SWAP_RESTART, restartSwap);
}
export function* bityOrderTimeRemaining(): SagaIterator {
while (true) {
let hasShownNotification = false;
while (true) {
yield call(delay, ONE_SECOND);
const swap = yield select(getSwap);
// if (swap.bityOrder.status === 'OPEN') {
const createdTimeStampMoment = moment(swap.orderTimestampCreatedISOString);
const validUntil = moment(createdTimeStampMoment).add(swap.validFor, 's');
const now = moment();
@ -115,38 +225,136 @@ export function* bityTimeRemaining(): SagaIterator {
const duration = moment.duration(validUntil.diff(now));
const seconds = duration.asSeconds();
yield put(orderTimeSwap(parseInt(seconds.toString(), 10)));
// TODO (!Important) - check orderStatus here and stop polling / show notifications based on status
switch (swap.bityOrderStatus) {
case 'CANC':
yield put(stopPollBityOrderStatus());
yield put(stopLoadBityRatesSwap());
yield put(stopOrderTimerSwap());
if (!hasShownNotification) {
hasShownNotification = true;
yield put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity));
}
break;
case 'FILL':
yield put(stopPollBityOrderStatus());
yield put(stopLoadBityRatesSwap());
yield put(stopOrderTimerSwap());
break;
}
} else {
switch (swap.orderStatus) {
switch (swap.bityOrderStatus) {
case 'OPEN':
yield put(orderTimeSwap(0));
yield put(stopPollBityOrderStatus());
yield put({ type: 'SWAP_STOP_LOAD_BITY_RATES' });
yield put(stopLoadBityRatesSwap());
if (!hasShownNotification) {
hasShownNotification = true;
yield put(showNotification('danger', BITY_TIMEOUT_MESSAGE, Infinity));
yield put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity));
}
break;
case 'CANC':
yield put(stopPollBityOrderStatus());
yield put({ type: 'SWAP_STOP_LOAD_BITY_RATES' });
yield put(stopLoadBityRatesSwap());
if (!hasShownNotification) {
hasShownNotification = true;
yield put(showNotification('danger', BITY_TIMEOUT_MESSAGE, Infinity));
yield put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity));
}
break;
case 'RCVE':
if (!hasShownNotification) {
hasShownNotification = true;
yield put(showNotification('warning', BITY_TIMEOUT_MESSAGE, Infinity));
yield put(showNotification('warning', ORDER_TIMEOUT_MESSAGE, Infinity));
}
break;
case 'FILL':
yield put(stopPollBityOrderStatus());
yield put({ type: 'SWAP_STOP_LOAD_BITY_RATES' });
yield put(stopLoadBityRatesSwap());
yield put(stopOrderTimerSwap());
break;
}
}
}
}
}
export function* shapeshiftOrderTimeRemaining(): SagaIterator {
while (true) {
let hasShownNotification = false;
while (true) {
yield call(delay, ONE_SECOND);
const swap = yield select(getSwap);
const createdTimeStampMoment = moment(swap.orderTimestampCreatedISOString);
const validUntil = moment(createdTimeStampMoment).add(swap.validFor, 's');
const now = moment();
if (validUntil.isAfter(now)) {
const duration = moment.duration(validUntil.diff(now));
const seconds = duration.asSeconds();
yield put(orderTimeSwap(parseInt(seconds.toString(), 10)));
switch (swap.shapeshiftOrderStatus) {
case 'failed':
yield put(stopPollShapeshiftOrderStatus());
yield put(stopLoadShapeshiftRatesSwap());
yield put(stopOrderTimerSwap());
if (!hasShownNotification) {
hasShownNotification = true;
yield put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity));
}
break;
case 'complete':
yield put(stopPollShapeshiftOrderStatus());
yield put(stopLoadShapeshiftRatesSwap());
yield put(stopOrderTimerSwap());
break;
}
} else {
switch (swap.shapeshiftOrderStatus) {
case 'no_deposits':
yield put(orderTimeSwap(0));
yield put(stopPollShapeshiftOrderStatus());
yield put(stopLoadShapeshiftRatesSwap());
if (!hasShownNotification) {
hasShownNotification = true;
yield put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity));
}
break;
case 'failed':
yield put(stopPollShapeshiftOrderStatus());
yield put(stopLoadShapeshiftRatesSwap());
if (!hasShownNotification) {
hasShownNotification = true;
yield put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity));
}
break;
case 'received':
if (!hasShownNotification) {
hasShownNotification = true;
yield put(showNotification('warning', ORDER_RECEIVED_MESSAGE, Infinity));
}
break;
case 'complete':
yield put(stopPollShapeshiftOrderStatus());
yield put(stopLoadShapeshiftRatesSwap());
yield put(stopOrderTimerSwap());
break;
}
}
}
}
}
export function* handleOrderTimeRemaining(): SagaIterator {
const swap = yield select(getSwap);
let orderTimeRemainingTask;
if (swap.provider === 'shapeshift') {
orderTimeRemainingTask = yield fork(shapeshiftOrderTimeRemaining);
} else {
orderTimeRemainingTask = yield fork(bityOrderTimeRemaining);
}
yield take(TypeKeys.SWAP_ORDER_STOP_TIMER);
yield cancel(orderTimeRemainingTask);
}
export function* swapTimerSaga(): SagaIterator {
yield takeEvery(TypeKeys.SWAP_ORDER_START_TIMER, handleOrderTimeRemaining);
}

View File

@ -1,11 +1,19 @@
import { showNotification } from 'actions/notifications';
import { loadBityRatesSucceededSwap } from 'actions/swap';
import {
loadBityRatesSucceededSwap,
loadShapeshiftRatesSucceededSwap,
changeSwapProvider,
ChangeProviderSwapAcion
} from 'actions/swap';
import { TypeKeys } from 'actions/swap/constants';
import { getAllRates } from 'api/bity';
import { delay, SagaIterator } from 'redux-saga';
import { call, cancel, fork, put, take, takeLatest } from 'redux-saga/effects';
import { call, select, cancel, fork, put, take, takeLatest, race } from 'redux-saga/effects';
import shapeshift from 'api/shapeshift';
import { getSwap } from 'sagas/swap/orders';
const POLLING_CYCLE = 30000;
export const SHAPESHIFT_TIMEOUT = 10000;
export function* loadBityRates(): SagaIterator {
while (true) {
@ -19,6 +27,37 @@ export function* loadBityRates(): SagaIterator {
}
}
export function* loadShapeshiftRates(): SagaIterator {
while (true) {
try {
// Race b/w api call and timeout
// getShapeShiftRates should be an api call that accepts a whitelisted arr of symbols
const { tokens } = yield race({
tokens: call(shapeshift.getAllRates),
timeout: call(delay, SHAPESHIFT_TIMEOUT)
});
// If tokens exist, put it into the redux state, otherwise switch to bity.
if (tokens) {
yield put(loadShapeshiftRatesSucceededSwap(tokens));
} else {
yield put(
showNotification('danger', 'Error loading ShapeShift tokens - reverting to Bity')
);
}
} catch (error) {
yield put(showNotification('danger', `Error loading ShapeShift tokens - ${error}`));
}
yield call(delay, POLLING_CYCLE);
}
}
export function* swapProvider(action: ChangeProviderSwapAcion): SagaIterator {
const swap = yield select(getSwap);
if (swap.provider !== action.payload) {
yield put(changeSwapProvider(action.payload));
}
}
// Fork our recurring API call, watch for the need to cancel.
export function* handleBityRates(): SagaIterator {
const loadBityRatesTask = yield fork(loadBityRates);
@ -30,3 +69,20 @@ export function* handleBityRates(): SagaIterator {
export function* getBityRatesSaga(): SagaIterator {
yield takeLatest(TypeKeys.SWAP_LOAD_BITY_RATES_REQUESTED, handleBityRates);
}
// Fork our API call
export function* handleShapeShiftRates(): SagaIterator {
const loadShapeShiftRatesTask = yield fork(loadShapeshiftRates);
yield take(TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES);
yield cancel(loadShapeShiftRatesTask);
}
// Watch for SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED action.
export function* getShapeShiftRatesSaga(): SagaIterator {
yield takeLatest(TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED, handleShapeShiftRates);
}
// Watch for provider swaps
export function* swapProviderSaga(): SagaIterator {
yield takeLatest(TypeKeys.SWAP_CHANGE_PROVIDER, swapProvider);
}

View File

@ -60,8 +60,8 @@ export function* estimateGas(): SagaIterator {
while (true) {
const { payload }: EstimateGasRequestedAction = yield take(requestChan);
// debounce 1000 ms
yield call(delay, 1000);
// debounce 250 ms
yield call(delay, 250);
const node: INode = yield select(getNodeLib);
const walletInst: IWallet = yield select(getWalletInst);
try {

View File

@ -13,7 +13,10 @@ import {
UnlockPrivateKeyAction,
ScanWalletForTokensAction,
SetWalletTokensAction,
TypeKeys
TypeKeys,
SetTokenBalancePendingAction,
setTokenBalanceFulfilled,
setTokenBalanceRejected
} from 'actions/wallet';
import { Wei } from 'libs/units';
import { changeNodeIntent, web3UnsetNode, TypeKeys as ConfigTypeKeys } from 'actions/config';
@ -27,10 +30,10 @@ import {
Web3Wallet,
WalletConfig
} from 'libs/wallet';
import { NODES, initWeb3Node } from 'config/data';
import { NODES, initWeb3Node, Token } from 'config/data';
import { SagaIterator } from 'redux-saga';
import { apply, call, fork, put, select, takeEvery, take } from 'redux-saga/effects';
import { getNodeLib } from 'selectors/config';
import { getNodeLib, getAllTokens } from 'selectors/config';
import {
getTokens,
getWalletInst,
@ -80,6 +83,30 @@ export function* updateTokenBalances(): SagaIterator {
}
}
export function* updateTokenBalance(action: SetTokenBalancePendingAction): SagaIterator {
try {
const wallet: null | IWallet = yield select(getWalletInst);
const { tokenSymbol } = action.payload;
const allTokens: Token[] = yield select(getAllTokens);
const token = allTokens.find(t => t.symbol === tokenSymbol);
if (!wallet) {
return;
}
if (!token) {
throw Error('Token not found');
}
const tokenBalances: TokenBalanceLookup = yield call(getTokenBalances, wallet, [token]);
yield put(setTokenBalanceFulfilled(tokenBalances));
} catch (error) {
console.error('Failed to get token balance', error);
yield put(setTokenBalanceRejected());
}
}
export function* scanWalletForTokens(action: ScanWalletForTokensAction): SagaIterator {
try {
const wallet = action.payload;
@ -229,6 +256,7 @@ export default function* walletSaga(): SagaIterator {
takeEvery(TypeKeys.WALLET_SET, handleNewWallet),
takeEvery(TypeKeys.WALLET_SCAN_WALLET_FOR_TOKENS, scanWalletForTokens),
takeEvery(TypeKeys.WALLET_SET_WALLET_TOKENS, handleSetWalletTokens),
takeEvery(CustomTokenTypeKeys.CUSTOM_TOKEN_ADD, handleCustomTokenAdd)
takeEvery(CustomTokenTypeKeys.CUSTOM_TOKEN_ADD, handleCustomTokenAdd),
takeEvery(TypeKeys.WALLET_SET_TOKEN_BALANCE_PENDING, updateTokenBalance)
];
}

View File

@ -9,6 +9,8 @@ import {
import { INode } from 'libs/nodes/INode';
import { AppState } from 'reducers';
import { getNetworkConfigFromId } from 'utils/network';
import { isEtherUnit } from 'libs/units';
import { SHAPESHIFT_TOKEN_WHITELIST } from 'api/shapeshift';
export function getNode(state: AppState): string {
return state.config.nodeSelection;
@ -41,6 +43,12 @@ export function getAllTokens(state: AppState): Token[] {
return networkTokens.concat(state.customTokens);
}
export function tokenExists(state: AppState, token: string): boolean {
const existInWhitelist = SHAPESHIFT_TOKEN_WHITELIST.includes(token);
const existsInNetwork = !!getAllTokens(state).find(t => t.symbol === token);
return existsInNetwork || existInWhitelist;
}
export function getLanguageSelection(state: AppState): string {
return state.config.languageSelection;
}
@ -62,3 +70,12 @@ export function getForceOffline(state: AppState): boolean {
}
export const isAnyOffline = (state: AppState) => getOffline(state) || getForceOffline(state);
export function isSupportedUnit(state: AppState, unit: string) {
const isToken: boolean = tokenExists(state, unit);
const isEther: boolean = isEtherUnit(unit);
if (!isToken && !isEther) {
return false;
}
return true;
}

6
common/selectors/swap.ts Normal file
View File

@ -0,0 +1,6 @@
import { AppState } from 'reducers';
const getSwap = (state: AppState) => state.swap;
export const getOrigin = (state: AppState) => getSwap(state).origin;
export const getPaymentAddress = (state: AppState) => getSwap(state).paymentAddress;
export const shouldDisplayLiteSend = (state: AppState) => getSwap(state).showLiteSend;

View File

@ -76,11 +76,15 @@ export function getTokenBalances(state: AppState, nonZeroOnly: boolean = false):
}
export const getTokenBalance = (state: AppState, unit: string): TokenValue | null => {
return getTokenWithBalance(state, unit).balance;
const token = getTokenWithBalance(state, unit);
if (!token) {
return token;
}
return token.balance;
};
export const getTokenWithBalance = (state: AppState, unit: string): TokenBalance => {
const tokens = getTokenBalances(state, true);
const tokens = getTokenBalances(state, false);
const currentToken = tokens.filter(t => t.symbol === unit);
//TODO: getting the first index is kinda hacky
return currentToken[0];

View File

@ -130,8 +130,18 @@ const configureStore = () => {
},
swap: {
...state.swap,
options: {},
bityRates: {}
options: {
byId: {},
allIds: []
},
bityRates: {
byId: {},
allIds: []
},
shapeshiftRates: {
byId: {},
allIds: []
}
},
customTokens: state.customTokens
});

View File

@ -1,3 +1,11 @@
import has from 'lodash/has';
export function objectContainsObjectKeys(checkingObject, containingObject) {
const checkingObjectKeys = Object.keys(checkingObject);
const containsAll = checkingObjectKeys.map(key => has(containingObject, key));
return containsAll.every(isTrue => isTrue);
}
export function getKeyByValue(object, value) {
return Object.keys(object).find(key => object[key] === value);
}

View File

@ -4,6 +4,7 @@ exports[`render snapshot 1`] = `
<Swap
bityOrder={Object {}}
bityOrderCreateRequestedSwap={[Function]}
bityOrderStatus={null}
bityRates={
Object {
"allIds": Array [],
@ -23,13 +24,13 @@ exports[`render snapshot 1`] = `
isFetchingRates={null}
isPostingOrder={false}
loadBityRatesRequestedSwap={[Function]}
loadShapeshiftRatesRequestedSwap={[Function]}
options={
Object {
"allIds": Array [],
"byId": Object {},
}
}
orderStatus={null}
origin={
Object {
"amount": NaN,
@ -38,14 +39,28 @@ exports[`render snapshot 1`] = `
}
outputTx={null}
paymentAddress={null}
provider="bity"
restartSwap={[Function]}
secondsRemaining={null}
shapeshiftOrder={Object {}}
shapeshiftOrderCreateRequestedSwap={[Function]}
shapeshiftOrderStatus={null}
shapeshiftRates={
Object {
"allIds": Array [],
"byId": Object {},
}
}
showNotification={[Function]}
startOrderTimerSwap={[Function]}
startPollBityOrderStatus={[Function]}
startPollShapeshiftOrderStatus={[Function]}
step={1}
stopLoadBityRatesSwap={[Function]}
stopLoadShapeshiftRatesSwap={[Function]}
stopOrderTimerSwap={[Function]}
stopPollBityOrderStatus={[Function]}
stopPollShapeshiftOrderStatus={[Function]}
swapProvider={[Function]}
/>
`;

View File

@ -1,11 +1,39 @@
import { swap, INITIAL_STATE } from 'reducers/swap';
import * as swapActions from 'actions/swap';
import { NormalizedBityRates, NormalizedOptions } from 'reducers/swap/types';
import {
NormalizedBityRates,
NormalizedOptions,
NormalizedShapeshiftRates
} from 'reducers/swap/types';
import { normalize } from 'normalizr';
import * as schema from 'reducers/swap/schema';
import { TypeKeys } from 'actions/swap/constants';
describe('swap reducer', () => {
const apiResponse = {
const shapeshiftApiResponse = {
['1SSTANT']: {
id: '1STANT',
options: [
{
id: '1ST',
status: 'available',
image: 'https://shapeshift.io/images/coins/firstblood.png',
name: 'FirstBlood'
},
{
id: 'ANT',
status: 'available',
image: 'https://shapeshift.io/images/coins/aragon.png',
name: 'Aragon'
}
],
rate: '0.24707537',
limit: 5908.29166225,
min: 7.86382979
}
};
const bityApiResponse = {
BTCETH: {
id: 'BTCETH',
options: [{ id: 'BTC' }, { id: 'ETH' }],
@ -17,20 +45,43 @@ describe('swap reducer', () => {
rate: 0.042958
}
};
const normalizedbityRates: NormalizedBityRates = {
byId: normalize(apiResponse, [schema.bityRate]).entities.bityRates,
allIds: schema.allIds(normalize(apiResponse, [schema.bityRate]).entities.bityRates)
const normalizedBityRates: NormalizedBityRates = {
byId: normalize(bityApiResponse, [schema.providerRate]).entities.providerRates,
allIds: schema.allIds(normalize(bityApiResponse, [schema.providerRate]).entities.providerRates)
};
const normalizedOptions: NormalizedOptions = {
byId: normalize(apiResponse, [schema.bityRate]).entities.options,
allIds: schema.allIds(normalize(apiResponse, [schema.bityRate]).entities.options)
const normalizedShapeshiftRates: NormalizedShapeshiftRates = {
byId: normalize(shapeshiftApiResponse, [schema.providerRate]).entities.providerRates,
allIds: schema.allIds(
normalize(shapeshiftApiResponse, [schema.providerRate]).entities.providerRates
)
};
const normalizedBityOptions: NormalizedOptions = {
byId: normalize(bityApiResponse, [schema.providerRate]).entities.options,
allIds: schema.allIds(normalize(bityApiResponse, [schema.providerRate]).entities.options)
};
const normalizedShapeshiftOptions: NormalizedOptions = {
byId: normalize(shapeshiftApiResponse, [schema.providerRate]).entities.options,
allIds: schema.allIds(normalize(shapeshiftApiResponse, [schema.providerRate]).entities.options)
};
it('should handle SWAP_LOAD_BITY_RATES_SUCCEEDED', () => {
expect(swap(undefined, swapActions.loadBityRatesSucceededSwap(apiResponse))).toEqual({
expect(swap(undefined, swapActions.loadBityRatesSucceededSwap(bityApiResponse))).toEqual({
...INITIAL_STATE,
isFetchingRates: false,
bityRates: normalizedbityRates,
options: normalizedOptions
bityRates: normalizedBityRates,
options: normalizedBityOptions
});
});
it('should handle SWAP_LOAD_SHAPESHIFT_RATES_SUCCEEDED', () => {
expect(
swap(undefined, swapActions.loadShapeshiftRatesSucceededSwap(shapeshiftApiResponse))
).toEqual({
...INITIAL_STATE,
isFetchingRates: false,
shapeshiftRates: normalizedShapeshiftRates,
options: normalizedShapeshiftOptions
});
});
@ -55,7 +106,8 @@ describe('swap reducer', () => {
swap(
{
...INITIAL_STATE,
bityRates: normalizedbityRates,
bityRates: normalizedBityRates,
shapeshiftRates: normalizedShapeshiftRates,
origin: { id: 'BTC', amount: 1 },
destination: { id: 'ETH', amount: 3 }
},
@ -63,14 +115,15 @@ describe('swap reducer', () => {
)
).toEqual({
...INITIAL_STATE,
bityRates: normalizedbityRates
bityRates: normalizedBityRates,
shapeshiftRates: normalizedShapeshiftRates
});
});
it('should handle SWAP_ORDER_CREATE_REQUESTED', () => {
it('should handle SWAP_BITY_ORDER_CREATE_REQUESTED', () => {
expect(
swap(undefined, {
type: 'SWAP_ORDER_CREATE_REQUESTED'
type: TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED
} as swapActions.SwapAction)
).toEqual({
...INITIAL_STATE,
@ -78,10 +131,32 @@ describe('swap reducer', () => {
});
});
it('should handle SWAP_ORDER_CREATE_FAILED', () => {
it('should handle SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED', () => {
expect(
swap(undefined, {
type: 'SWAP_ORDER_CREATE_FAILED'
type: TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED
} as swapActions.SwapAction)
).toEqual({
...INITIAL_STATE,
isPostingOrder: true
});
});
it('should handle SWAP_BITY_ORDER_CREATE_FAILED', () => {
expect(
swap(undefined, {
type: TypeKeys.SWAP_BITY_ORDER_CREATE_FAILED
} as swapActions.SwapAction)
).toEqual({
...INITIAL_STATE,
isPostingOrder: false
});
});
it('should handle SWAP_SHAPESHIFT_ORDER_CREATE_FAILED', () => {
expect(
swap(undefined, {
type: TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_FAILED
} as swapActions.SwapAction)
).toEqual({
...INITIAL_STATE,
@ -122,11 +197,49 @@ describe('swap reducer', () => {
validFor: mockedBityOrder.validFor,
orderTimestampCreatedISOString: mockedBityOrder.timestamp_created,
paymentAddress: mockedBityOrder.payment_address,
orderStatus: mockedBityOrder.status,
bityOrderStatus: mockedBityOrder.status,
orderId: mockedBityOrder.id
});
});
it('should handle SWAP_SHAPESHIFT_ORDER_CREATE_SUCCEEDED', () => {
const mockedShapeshiftOrder: swapActions.ShapeshiftOrderResponse = {
orderId: '64d73218-0ee9-4c6c-9bbd-6da9208595f5',
pair: 'eth_ant',
withdrawal: '0x6b3a639eb96d8e0241fe4e114d99e739f906944e',
withdrawalAmount: '200.13550988',
deposit: '0x039ed77933388642fdd618d27bfc4fa3582d10c4',
depositAmount: '0.98872802',
expiration: 1514633757288,
quotedRate: '203.47912271',
maxLimit: 7.04575258,
apiPubKey:
'0ca1ccd50b708a3f8c02327f0caeeece06d3ddc1b0ac749a987b453ee0f4a29bdb5da2e53bc35e57fb4bb7ae1f43c93bb098c3c4716375fc1001c55d8c94c160',
minerFee: '1.05'
};
const swapState = swap(
undefined,
swapActions.shapeshiftOrderCreateSucceededSwap(mockedShapeshiftOrder)
);
expect(swapState).toEqual({
...INITIAL_STATE,
shapeshiftOrder: {
...mockedShapeshiftOrder
},
isPostingOrder: false,
originAmount: parseFloat(mockedShapeshiftOrder.depositAmount),
destinationAmount: parseFloat(mockedShapeshiftOrder.withdrawalAmount),
secondsRemaining: swapState.secondsRemaining,
validFor: swapState.validFor,
orderTimestampCreatedISOString: swapState.orderTimestampCreatedISOString,
paymentAddress: mockedShapeshiftOrder.deposit,
shapeshiftOrderStatus: 'no_deposits',
orderId: mockedShapeshiftOrder.orderId
});
});
it('should handle SWAP_BITY_ORDER_STATUS_SUCCEEDED', () => {
const mockedBityResponse: swapActions.BityOrderResponse = {
input: {
@ -144,10 +257,25 @@ describe('swap reducer', () => {
status: 'status'
};
expect(swap(undefined, swapActions.orderStatusSucceededSwap(mockedBityResponse))).toEqual({
expect(swap(undefined, swapActions.bityOrderStatusSucceededSwap(mockedBityResponse))).toEqual({
...INITIAL_STATE,
outputTx: mockedBityResponse.output.reference,
orderStatus: mockedBityResponse.output.status
bityOrderStatus: mockedBityResponse.output.status
});
});
it('should handle SWAP_SHAPESHIFT_ORDER_STATUS_SUCCEEDED', () => {
const mockedShapeshiftResponse: swapActions.ShapeshiftStatusResponse = {
status: 'complete',
transaction: '0x039ed77933388642fdd618d27bfc4fa3582d10c4'
};
expect(
swap(undefined, swapActions.shapeshiftOrderStatusSucceededSwap(mockedShapeshiftResponse))
).toEqual({
...INITIAL_STATE,
shapeshiftOrderStatus: mockedShapeshiftResponse.status,
outputTx: mockedShapeshiftResponse.transaction
});
});
@ -170,6 +298,17 @@ describe('swap reducer', () => {
});
});
it('should handle SWAP_LOAD_SHAPESHIFT_RATE_REQUESTED', () => {
expect(
swap(undefined, {
type: TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED
} as swapActions.SwapAction)
).toEqual({
...INITIAL_STATE,
isFetchingRates: true
});
});
it('should handle SWAP_STOP_LOAD_BITY_RATES', () => {
expect(
swap(undefined, {
@ -180,4 +319,15 @@ describe('swap reducer', () => {
isFetchingRates: false
});
});
it('should handle SWAP_STOP_LOAD_SHAPESHIFT_RATES', () => {
expect(
swap(undefined, {
type: TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES
} as swapActions.SwapAction)
).toEqual({
...INITIAL_STATE,
isFetchingRates: false
});
});
});

View File

@ -0,0 +1,34 @@
import { configuredStore } from 'store';
import { cloneableGenerator, createMockTask } from 'redux-saga/utils';
import { take, race, fork } from 'redux-saga/effects';
import { TypeKeys as TransactionTK } from 'actions/transaction';
import { TypeKeys as WalletTK } from 'actions/wallet';
import { TypeKeys as SwapTK } from 'actions/swap/constants';
import { configureLiteSendSaga, handleConfigureLiteSend } from 'sagas/swap/liteSend';
// init module
configuredStore.getState();
describe('Testing handle configure lite send', () => {
const generators = {
original: cloneableGenerator(handleConfigureLiteSend)()
};
const { original } = generators;
it('forks a configureLiteSend saga', () => {
const expectedYield = fork(configureLiteSendSaga);
expect(original.next().value).toEqual(expectedYield);
});
it('races between three conditions, either the transaction state is reset, the user navigated away from the page, or bitty/shapeshift polling as finished', () => {
const mockedTask = createMockTask();
const expectedYield = race({
transactionReset: take(TransactionTK.RESET),
userNavigatedAway: take(WalletTK.WALLET_RESET),
bityPollingFinished: take(SwapTK.SWAP_STOP_POLL_BITY_ORDER_STATUS),
shapeshiftPollingFinished: take(SwapTK.SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS)
});
expect(original.next(mockedTask).value).toEqual(expectedYield);
});
});

View File

@ -8,23 +8,32 @@ import {
BityOrderOutput,
BityOrderResponse,
changeStepSwap,
orderStatusRequestedSwap,
orderStatusSucceededSwap,
bityOrderStatusRequested,
bityOrderStatusSucceededSwap,
orderTimeSwap,
startOrderTimerSwap,
startPollBityOrderStatus,
stopLoadBityRatesSwap,
stopPollBityOrderStatus
stopPollBityOrderStatus,
startPollShapeshiftOrderStatus,
shapeshiftOrderStatusRequested,
shapeshiftOrderStatusSucceededSwap,
shapeshiftOrderCreateRequestedSwap,
shapeshiftOrderCreateSucceededSwap,
shapeshiftOrderCreateFailedSwap,
stopLoadShapeshiftRatesSwap,
ShapeshiftOrderResponse,
stopPollShapeshiftOrderStatus,
stopOrderTimerSwap
} from 'actions/swap';
import { getOrderStatus, postOrder } from 'api/bity';
import {
State as SwapState,
INITIAL_STATE as INITIAL_SWAP_STATE
} from 'reducers/swap';
import shapeshift from 'api/shapeshift';
import { State as SwapState, INITIAL_STATE as INITIAL_SWAP_STATE } from 'reducers/swap';
import { delay } from 'redux-saga';
import {
call,
cancel,
apply,
cancelled,
fork,
put,
@ -38,10 +47,17 @@ import {
pollBityOrderStatusSaga,
postBityOrderCreate,
postBityOrderSaga,
bityTimeRemaining,
BITY_TIMEOUT_MESSAGE
pollShapeshiftOrderStatus,
pollShapeshiftOrderStatusSaga,
postShapeshiftOrderSaga,
shapeshiftOrderTimeRemaining,
bityOrderTimeRemaining,
ORDER_TIMEOUT_MESSAGE,
postShapeshiftOrderCreate,
ORDER_RECEIVED_MESSAGE
} from 'sagas/swap/orders';
import { cloneableGenerator, createMockTask } from 'redux-saga/utils';
import { TypeKeys } from 'actions/swap/constants';
const ONE_SECOND = 1000;
const TEN_SECONDS = ONE_SECOND * 10;
@ -96,34 +112,24 @@ describe('pollBityOrderStatus*', () => {
expect(data.gen.next().value).toEqual(select(getSwap));
});
it('should put orderStatusRequestedSwap', () => {
expect(data.gen.next(fakeSwap).value).toEqual(
put(orderStatusRequestedSwap())
);
it('should put bityOrderStatusRequestedSwap', () => {
expect(data.gen.next(fakeSwap).value).toEqual(put(bityOrderStatusRequested()));
});
it('should call getOrderStatus with swap.orderId', () => {
expect(data.gen.next().value).toEqual(
call(getOrderStatus, fakeSwap.orderId)
);
expect(data.gen.next().value).toEqual(call(getOrderStatus, fakeSwap.orderId));
});
it('should put showNotfication on error', () => {
data.clone = data.gen.clone();
expect(data.clone.next(errorStatus).value).toEqual(
put(
showNotification(
'danger',
`Bity Error: ${errorStatus.msg}`,
TEN_SECONDS
)
)
put(showNotification('danger', `Bity Error: ${errorStatus.msg}`, TEN_SECONDS))
);
});
it('should put orderStatusSucceededSwap', () => {
expect(data.gen.next(successStatus).value).toEqual(
put(orderStatusSucceededSwap(successStatus.data))
put(bityOrderStatusSucceededSwap(successStatus.data))
);
});
@ -142,10 +148,81 @@ describe('pollBityOrderStatus*', () => {
});
it('should restart loop', () => {
expect(data.gen.next(fakeSwap).value).toEqual(
put(orderStatusRequestedSwap())
expect(data.gen.next(fakeSwap).value).toEqual(put(bityOrderStatusRequested()));
});
});
describe('pollShapeshiftOrderStatus*', () => {
const data = {} as any;
data.gen = cloneableGenerator(pollShapeshiftOrderStatus)();
const fakeSwap: SwapState = {
...INITIAL_SWAP_STATE,
orderId: '1'
};
const cancelledSwap = 'CANC';
const successStatus = {
status: 'complete',
transaction: '0x'
};
const errorStatus = {
error: 'Shapeshift error',
status: 'failed'
};
let random;
beforeAll(() => {
random = Math.random;
Math.random = () => 0.001;
});
afterAll(() => {
Math.random = random;
});
it('should select getSwap', () => {
expect(data.gen.next().value).toEqual(select(getSwap));
});
it('should put shapeshiftOrderStatusRequestedSwap', () => {
expect(data.gen.next(fakeSwap).value).toEqual(put(shapeshiftOrderStatusRequested()));
});
it('should apply shapeshift.checkStatus with swap.paymentAddress', () => {
expect(data.gen.next().value).toEqual(
apply(shapeshift, shapeshift.checkStatus, [fakeSwap.paymentAddress])
);
});
it('should put showNotfication on error', () => {
data.clone = data.gen.clone();
expect(data.clone.next(errorStatus).value).toEqual(
put(showNotification('danger', `Shapeshift Error: ${errorStatus.error}`, Infinity))
);
});
it('should put shapeshiftOrderStatusSucceededSwap', () => {
expect(data.gen.next(successStatus).value).toEqual(
put(shapeshiftOrderStatusSucceededSwap(successStatus))
);
});
it('should call delay for 5 seconds', () => {
expect(data.gen.next().value).toEqual(call(delay, ONE_SECOND * 5));
});
it('should select getSwap', () => {
expect(data.gen.next().value).toEqual(select(getSwap));
});
it('should break loop if swap is cancelled', () => {
data.clone2 = data.gen.clone();
expect(data.clone2.next(cancelledSwap).value).toEqual(cancelled());
expect(data.clone2.next().done).toEqual(true);
});
it('should restart loop', () => {
expect(data.gen.next(fakeSwap).value).toEqual(put(shapeshiftOrderStatusRequested()));
});
});
describe('pollBityOrderStatusSaga*', () => {
@ -154,9 +231,7 @@ describe('pollBityOrderStatusSaga*', () => {
const mockedTask = createMockTask();
it('should take SWAP_START_POLL_BITY_ORDER_STATUS', () => {
expect(data.gen.next().value).toEqual(
take('SWAP_START_POLL_BITY_ORDER_STATUS')
);
expect(data.gen.next().value).toEqual(take(TypeKeys.SWAP_START_POLL_BITY_ORDER_STATUS));
});
it('should be done if order status is false', () => {
@ -170,7 +245,7 @@ describe('pollBityOrderStatusSaga*', () => {
it('should take SWAP_STOP_POLL_BITY_ORDER_STATUS', () => {
expect(data.gen.next(mockedTask).value).toEqual(
take('SWAP_STOP_POLL_BITY_ORDER_STATUS')
take(TypeKeys.SWAP_STOP_POLL_BITY_ORDER_STATUS)
);
});
@ -179,6 +254,35 @@ describe('pollBityOrderStatusSaga*', () => {
});
});
describe('pollShapeshiftOrderStatusSaga*', () => {
const data = {} as any;
data.gen = cloneableGenerator(pollShapeshiftOrderStatusSaga)();
const mockedTask = createMockTask();
it('should take SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS', () => {
expect(data.gen.next().value).toEqual(take(TypeKeys.SWAP_START_POLL_SHAPESHIFT_ORDER_STATUS));
});
it('should be done if order status is false', () => {
data.clone = data.gen.clone();
expect(data.clone.next(false).done).toEqual(true);
});
it('should fork pollShapeshiftOrderStatus', () => {
expect(data.gen.next(true).value).toEqual(fork(pollShapeshiftOrderStatus));
});
it('should take SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS', () => {
expect(data.gen.next(mockedTask).value).toEqual(
take(TypeKeys.SWAP_STOP_POLL_SHAPESHIFT_ORDER_STATUS)
);
});
it('should cancel pollShapeshiftOrderStatusTask', () => {
expect(data.gen.next().value).toEqual(cancel(mockedTask));
});
});
describe('postBityOrderCreate*', () => {
const amount = 100;
const destinationAddress = '0x0';
@ -252,25 +356,126 @@ describe('postBityOrderCreate*', () => {
it('should handle an errored order', () => {
expect(data.clone2.next(errorOrder).value).toEqual(
put(
showNotification('danger', `Bity Error: ${errorOrder.msg}`, TEN_SECONDS)
)
put(showNotification('danger', `Bity Error: ${errorOrder.msg}`, TEN_SECONDS))
);
expect(data.clone2.next().value).toEqual(put(bityOrderCreateFailedSwap()));
});
});
describe('postShapeshiftOrderCreate*', () => {
const amount = 100;
const withdrawalAddress = '0x0';
const originKind = 'BAT';
const destKind = 'ETH';
const action = shapeshiftOrderCreateRequestedSwap(
withdrawalAddress,
originKind,
destKind,
amount
);
const orderResp: ShapeshiftOrderResponse = {
deposit: '0x0',
depositAmount: '0',
expiration: 100,
maxLimit: 1,
minerFee: '0.1',
orderId: '1',
pair: 'BTC_ETH',
quotedRate: '1',
withdrawal: '0x0',
withdrawalAmount: '2'
};
const successOrder = { success: orderResp };
const errorOrder = { error: 'message' };
const connectionErrMsg =
'Connection Error. Please check the developer console for more details and/or contact support';
const data = {} as any;
data.gen = cloneableGenerator(postShapeshiftOrderCreate)(action);
let random;
beforeAll(() => {
random = Math.random;
Math.random = () => 0.001;
});
afterAll(() => {
Math.random = random;
});
it('should put stopLoadShapeshiftRatesSwap', () => {
expect(data.gen.next().value).toEqual(put(stopLoadShapeshiftRatesSwap()));
});
it('should call shapeshift.sendAmount', () => {
data.clone1 = data.gen.clone();
expect(data.gen.next().value).toEqual(
apply(shapeshift, shapeshift.sendAmount, [
action.payload.withdrawal,
action.payload.originKind,
action.payload.destinationKind,
action.payload.destinationAmount
])
);
});
it('should put shapeshiftOrderCreateSucceededSwap', () => {
data.clone2 = data.gen.clone();
expect(data.gen.next(successOrder).value).toEqual(
put(shapeshiftOrderCreateSucceededSwap(successOrder.success))
);
});
it('should put changeStepSwap', () => {
expect(data.gen.next().value).toEqual(put(changeStepSwap(3)));
});
it('should put startOrderTimerSwap', () => {
expect(data.gen.next().value).toEqual(put(startOrderTimerSwap()));
});
it('should put startPollShapeshiftOrderStatus', () => {
expect(data.gen.next().value).toEqual(put(startPollShapeshiftOrderStatus()));
});
// failure modes
it('should handle a connection exeception', () => {
expect(data.clone1.throw().value).toEqual(
put(showNotification('danger', connectionErrMsg, TEN_SECONDS))
);
expect(data.clone1.next().value).toEqual(put(shapeshiftOrderCreateFailedSwap()));
expect(data.clone1.next().done).toEqual(true);
});
it('should handle an errored order', () => {
expect(data.clone2.next(errorOrder).value).toEqual(
put(showNotification('danger', `Shapeshift Error: ${errorOrder.error}`, TEN_SECONDS))
);
expect(data.clone2.next().value).toEqual(put(shapeshiftOrderCreateFailedSwap()));
});
});
describe('postBityOrderSaga*', () => {
const gen = postBityOrderSaga();
it('should takeEvery SWAP_ORDER_CREATE_REQUESTED', () => {
it('should takeEvery SWAP_BITY_ORDER_CREATE_REQUESTED', () => {
expect(gen.next().value).toEqual(
takeEvery('SWAP_ORDER_CREATE_REQUESTED', postBityOrderCreate)
takeEvery(TypeKeys.SWAP_BITY_ORDER_CREATE_REQUESTED, postBityOrderCreate)
);
});
});
describe('bityTimeRemaining*', () => {
describe('postShapeshiftOrderSaga*', () => {
const gen = postShapeshiftOrderSaga();
it('should takeEvery SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED', () => {
expect(gen.next().value).toEqual(
takeEvery(TypeKeys.SWAP_SHAPESHIFT_ORDER_CREATE_REQUESTED, postShapeshiftOrderCreate)
);
});
});
describe('bityOrderTimeRemaining*', () => {
const orderTime = new Date().toISOString();
const orderTimeExpired = new Date().getTime() - ELEVEN_SECONDS;
const swapValidFor = 10; //seconds
@ -287,7 +492,7 @@ describe('bityTimeRemaining*', () => {
let random;
const data = {} as any;
data.gen = cloneableGenerator(bityTimeRemaining)();
data.gen = cloneableGenerator(bityOrderTimeRemaining)();
beforeAll(() => {
random = Math.random;
@ -298,15 +503,6 @@ describe('bityTimeRemaining*', () => {
Math.random = random;
});
it('should take SWAP_ORDER_START_TIMER', () => {
expect(data.gen.next().value).toEqual(take('SWAP_ORDER_START_TIMER'));
});
it('should break while loop when take SWAP_ORDER_START_TIMER is false', () => {
data.clone1 = data.gen.clone();
expect(data.clone1.next().done).toEqual(true);
});
it('should call delay of one second', () => {
expect(data.gen.next(true).value).toEqual(call(delay, ONE_SECOND));
});
@ -324,48 +520,122 @@ describe('bityTimeRemaining*', () => {
});
it('should handle an OPEN order state', () => {
const openOrder = { ...swapOrderExpired, orderStatus: 'OPEN' };
const openOrder = { ...swapOrderExpired, bityOrderStatus: 'OPEN' };
data.OPEN = data.gen.clone();
expect(data.OPEN.next(openOrder).value).toEqual(put(orderTimeSwap(0)));
expect(data.OPEN.next().value).toEqual(put(stopPollBityOrderStatus()));
expect(data.OPEN.next().value).toEqual(put({ type: TypeKeys.SWAP_STOP_LOAD_BITY_RATES }));
expect(data.OPEN.next().value).toEqual(
put({ type: 'SWAP_STOP_LOAD_BITY_RATES' })
);
expect(data.OPEN.next().value).toEqual(
put(showNotification('danger', BITY_TIMEOUT_MESSAGE, Infinity))
put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity))
);
});
it('should handle a CANC order state', () => {
const cancOrder = { ...swapOrderExpired, orderStatus: 'CANC' };
const cancOrder = { ...swapOrderExpired, bityOrderStatus: 'CANC' };
data.CANC = data.gen.clone();
expect(data.CANC.next(cancOrder).value).toEqual(
put(stopPollBityOrderStatus())
);
expect(data.CANC.next(cancOrder).value).toEqual(put(stopPollBityOrderStatus()));
expect(data.CANC.next().value).toEqual(put({ type: TypeKeys.SWAP_STOP_LOAD_BITY_RATES }));
expect(data.CANC.next().value).toEqual(
put({ type: 'SWAP_STOP_LOAD_BITY_RATES' })
);
expect(data.CANC.next().value).toEqual(
put(showNotification('danger', BITY_TIMEOUT_MESSAGE, Infinity))
put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity))
);
});
it('should handle a RCVE order state', () => {
const rcveOrder = { ...swapOrderExpired, orderStatus: 'RCVE' };
const rcveOrder = { ...swapOrderExpired, bityOrderStatus: 'RCVE' };
data.RCVE = data.gen.clone();
expect(data.RCVE.next(rcveOrder).value).toEqual(
put(showNotification('warning', BITY_TIMEOUT_MESSAGE, Infinity))
put(showNotification('warning', ORDER_TIMEOUT_MESSAGE, Infinity))
);
});
it('should handle a FILL order state', () => {
const fillOrder = { ...swapOrderExpired, orderStatus: 'FILL' };
const fillOrder = { ...swapOrderExpired, bityOrderStatus: 'FILL' };
data.FILL = data.gen.clone();
expect(data.FILL.next(fillOrder).value).toEqual(
put(stopPollBityOrderStatus())
);
expect(data.FILL.next().value).toEqual(
put({ type: 'SWAP_STOP_LOAD_BITY_RATES' })
);
expect(data.FILL.next(fillOrder).value).toEqual(put(stopPollBityOrderStatus()));
expect(data.FILL.next().value).toEqual(put({ type: TypeKeys.SWAP_STOP_LOAD_BITY_RATES }));
});
});
describe('shapeshiftOrderTimeRemaining*', () => {
const orderTime = new Date().toISOString();
const orderTimeExpired = new Date().getTime() - ELEVEN_SECONDS;
const swapValidFor = 10; //seconds
const swapOrder = {
...INITIAL_SWAP_STATE,
orderTimestampCreatedISOString: orderTime,
validFor: swapValidFor
};
const swapOrderExpired = {
...INITIAL_SWAP_STATE,
orderTimestampCreatedISOString: new Date(orderTimeExpired).toISOString(),
validFor: swapValidFor
};
let random;
const data = {} as any;
data.gen = cloneableGenerator(shapeshiftOrderTimeRemaining)();
beforeAll(() => {
random = Math.random;
Math.random = () => 0.001;
});
afterAll(() => {
Math.random = random;
});
it('should call delay of one second', () => {
expect(data.gen.next(true).value).toEqual(call(delay, ONE_SECOND));
});
it('should select getSwap', () => {
expect(data.gen.next().value).toEqual(select(getSwap));
});
it('should handle if isValidUntil.isAfter(now)', () => {
data.clone2 = data.gen.clone();
const result = data.clone2.next(swapOrder).value;
expect(result).toHaveProperty('PUT');
expect(result.PUT.action.type).toEqual('SWAP_ORDER_TIME');
expect(result.PUT.action.payload).toBeGreaterThan(0);
});
it('should handle an no_deposits order state', () => {
const openOrder = { ...swapOrderExpired, shapeshiftOrderStatus: 'no_deposits' };
data.OPEN = data.gen.clone();
expect(data.OPEN.next(openOrder).value).toEqual(put(orderTimeSwap(0)));
expect(data.OPEN.next().value).toEqual(put(stopPollShapeshiftOrderStatus()));
expect(data.OPEN.next().value).toEqual(put({ type: TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES }));
expect(data.OPEN.next().value).toEqual(
put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity))
);
});
it('should handle a failed order state', () => {
const cancOrder = { ...swapOrderExpired, shapeshiftOrderStatus: 'failed' };
data.CANC = data.gen.clone();
expect(data.CANC.next(cancOrder).value).toEqual(put(stopPollShapeshiftOrderStatus()));
expect(data.CANC.next().value).toEqual(put({ type: TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES }));
expect(data.CANC.next().value).toEqual(
put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity))
);
});
it('should handle a received order state', () => {
const rcveOrder = { ...swapOrderExpired, shapeshiftOrderStatus: 'received' };
data.RCVE = data.gen.clone();
expect(data.RCVE.next(rcveOrder).value).toEqual(
put(showNotification('warning', ORDER_RECEIVED_MESSAGE, Infinity))
);
});
it('should handle a complete order state', () => {
const fillOrder = { ...swapOrderExpired, shapeshiftOrderStatus: 'complete' };
data.COMPLETE = data.gen.clone();
expect(data.COMPLETE.next(fillOrder).value).toEqual(put(stopPollShapeshiftOrderStatus()));
expect(data.COMPLETE.next().value).toEqual(
put({ type: TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES })
);
expect(data.COMPLETE.next().value).toEqual(put(stopOrderTimerSwap()));
});
});

View File

@ -1,10 +1,20 @@
import { showNotification } from 'actions/notifications';
import { loadBityRatesSucceededSwap } from 'actions/swap';
import { loadBityRatesSucceededSwap, loadShapeshiftRatesSucceededSwap } from 'actions/swap';
import { getAllRates } from 'api/bity';
import { delay } from 'redux-saga';
import { call, cancel, fork, put, take, takeLatest } from 'redux-saga/effects';
import { call, cancel, fork, put, race, take, takeLatest } from 'redux-saga/effects';
import { createMockTask } from 'redux-saga/utils';
import { loadBityRates, handleBityRates, getBityRatesSaga } from 'sagas/swap/rates';
import {
loadBityRates,
handleBityRates,
getBityRatesSaga,
loadShapeshiftRates,
getShapeShiftRatesSaga,
SHAPESHIFT_TIMEOUT,
handleShapeShiftRates
} from 'sagas/swap/rates';
import shapeshift from 'api/shapeshift';
import { TypeKeys } from 'actions/swap/constants';
describe('loadBityRates*', () => {
const gen1 = loadBityRates();
@ -51,6 +61,71 @@ describe('loadBityRates*', () => {
});
});
describe('loadShapeshiftRates*', () => {
const gen1 = loadShapeshiftRates();
const gen2 = loadShapeshiftRates();
const apiResponse = {
['1SSTANT']: {
id: '1STANT',
options: [
{
id: '1ST',
status: 'available',
image: 'https://shapeshift.io/images/coins/firstblood.png',
name: 'FirstBlood'
},
{
id: 'ANT',
status: 'available',
image: 'https://shapeshift.io/images/coins/aragon.png',
name: 'Aragon'
}
],
rate: '0.24707537',
limit: 5908.29166225,
min: 7.86382979
}
};
let random;
beforeAll(() => {
random = Math.random;
Math.random = () => 0.001;
});
afterAll(() => {
Math.random = random;
});
it('should race shapeshift.getAllRates', () => {
expect(gen1.next().value).toEqual(
race({
tokens: call(shapeshift.getAllRates),
timeout: call(delay, SHAPESHIFT_TIMEOUT)
})
);
});
it('should put loadShapeshiftRatesSucceededSwap', () => {
expect(gen1.next({ tokens: apiResponse }).value).toEqual(
put(loadShapeshiftRatesSucceededSwap(apiResponse))
);
});
it('should call delay for 30 seconds', () => {
expect(gen1.next().value).toEqual(call(delay, 30000));
});
it('should handle an exception', () => {
const err = 'error';
gen2.next();
expect((gen2 as any).throw(err).value).toEqual(
put(showNotification('danger', `Error loading ShapeShift tokens - ${err}`))
);
});
});
describe('handleBityRates*', () => {
const gen = handleBityRates();
const mockTask = createMockTask();
@ -60,7 +135,7 @@ describe('handleBityRates*', () => {
});
it('should take SWAP_STOP_LOAD_BITY_RATES', () => {
expect(gen.next(mockTask).value).toEqual(take('SWAP_STOP_LOAD_BITY_RATES'));
expect(gen.next(mockTask).value).toEqual(take(TypeKeys.SWAP_STOP_LOAD_BITY_RATES));
});
it('should cancel loadBityRatesTask', () => {
@ -72,10 +147,43 @@ describe('handleBityRates*', () => {
});
});
describe('handleShapeshiftRates*', () => {
const gen = handleShapeShiftRates();
const mockTask = createMockTask();
it('should fork loadShapeshiftRates', () => {
expect(gen.next().value).toEqual(fork(loadShapeshiftRates));
});
it('should take SWAP_STOP_LOAD_BITY_RATES', () => {
expect(gen.next(mockTask).value).toEqual(take(TypeKeys.SWAP_STOP_LOAD_SHAPESHIFT_RATES));
});
it('should cancel loadShapeShiftRatesTask', () => {
expect(gen.next().value).toEqual(cancel(mockTask));
});
it('should be done', () => {
expect(gen.next().done).toEqual(true);
});
});
describe('getBityRatesSaga*', () => {
const gen = getBityRatesSaga();
it('should takeLatest SWAP_LOAD_RATES_REQUESTED', () => {
expect(gen.next().value).toEqual(takeLatest('SWAP_LOAD_BITY_RATES_REQUESTED', handleBityRates));
it('should takeLatest SWAP_LOAD_BITY_RATES_REQUESTED', () => {
expect(gen.next().value).toEqual(
takeLatest(TypeKeys.SWAP_LOAD_BITY_RATES_REQUESTED, handleBityRates)
);
});
});
describe('getShapeshiftRatesSaga*', () => {
const gen = getShapeShiftRatesSaga();
it('should takeLatest SWAP_LOAD_BITY_RATES_REQUESTED', () => {
expect(gen.next().value).toEqual(
takeLatest(TypeKeys.SWAP_LOAD_SHAPESHIFT_RATES_REQUESTED, handleShapeShiftRates)
);
});
});

View File

@ -107,7 +107,7 @@ describe('estimateGas*', () => {
});
it('should call delay', () => {
expect(gens.gen.next(action).value).toEqual(call(delay, 1000));
expect(gens.gen.next(action).value).toEqual(call(delay, 250));
});
it('should select getNodeLib', () => {

View File

@ -0,0 +1,34 @@
import { objectContainsObjectKeys } from 'utils/helpers';
describe('objectContainsObjectKeys', () => {
it('should return true when object contains all keys of another object', () => {
const checkingObject = {
a: 1,
b: 2,
c: 3
};
const containingObject = {
a: 1,
b: 2,
c: 3,
d: 4
};
expect(objectContainsObjectKeys(checkingObject, containingObject)).toBeTruthy();
});
it('should return false when object does not contain all keys of another object', () => {
const checkingObject = {
a: 1,
b: 2,
c: 3
};
const containingObject = {
a: 1
};
expect(objectContainsObjectKeys(checkingObject, containingObject)).toBeFalsy();
});
});