/ common / api / shapeshift.ts
shapeshift.ts
  1  import flatten from 'lodash/flatten';
  2  import uniqBy from 'lodash/uniqBy';
  3  
  4  import { checkHttpStatus, parseJSON } from 'api/utils';
  5  
  6  export const SHAPESHIFT_API_KEY =
  7    '8abde0f70ca69d5851702d57b10305705d7333e93263124cc2a2649dab7ff9cf86401fc8de7677e8edcd0e7f1eed5270b1b49be8806937ef95d64839e319e6d9';
  8  
  9  export const SHAPESHIFT_BASE_URL = 'https://shapeshift.io';
 10  
 11  export const SHAPESHIFT_TOKEN_WHITELIST = [
 12    'OMG',
 13    'REP',
 14    'SNT',
 15    'SNGLS',
 16    'ZRX',
 17    'SWT',
 18    'ANT',
 19    'BAT',
 20    'BNT',
 21    'CVC',
 22    'DNT',
 23    '1ST',
 24    'GNO',
 25    'GNT',
 26    'EDG',
 27    'FUN',
 28    'RLC',
 29    'TRST',
 30    'GUP'
 31  ];
 32  export const SHAPESHIFT_WHITELIST = [...SHAPESHIFT_TOKEN_WHITELIST, 'ETH', 'ETC', 'BTC', 'XMR'];
 33  
 34  interface IPairData {
 35    limit: number;
 36    maxLimit: number;
 37    min: number;
 38    minerFee: number;
 39    pair: string;
 40    rate: string;
 41  }
 42  
 43  interface IExtraPairData {
 44    status: string;
 45    image: string;
 46    name: string;
 47  }
 48  
 49  interface IAvailablePairData {
 50    [pairName: string]: IExtraPairData;
 51  }
 52  
 53  interface ShapeshiftMarketInfo {
 54    rate: string;
 55    limit: number;
 56    pair: string;
 57    maxLimit: number;
 58    min: number;
 59    minerFee: number;
 60  }
 61  
 62  interface TokenMap {
 63    [pairName: string]: {
 64      id: string;
 65      rate: string;
 66      limit: number;
 67      min: number;
 68      options: (IExtraPairData & { id: string })[];
 69    };
 70  }
 71  
 72  interface ShapeshiftCoinInfo {
 73    image: string;
 74    imageSmall: string;
 75    minerFee: number;
 76    name: string;
 77    status: string;
 78    symbol: string;
 79  }
 80  
 81  interface ShapeshiftCoinInfoMap {
 82    [id: string]: ShapeshiftCoinInfo;
 83  }
 84  
 85  interface ShapeshiftOption {
 86    id?: string;
 87    status?: string;
 88    image?: string;
 89  }
 90  
 91  interface ShapeshiftOptionMap {
 92    [symbol: string]: ShapeshiftOption;
 93  }
 94  
 95  class ShapeshiftService {
 96    public whitelist = SHAPESHIFT_WHITELIST;
 97    private url = SHAPESHIFT_BASE_URL;
 98    private apiKey = SHAPESHIFT_API_KEY;
 99    private postHeaders = {
100      'Content-Type': 'application/json'
101    };
102    private supportedCoinsAndTokens: ShapeshiftCoinInfoMap = {};
103    private fetchedSupportedCoinsAndTokens = false;
104  
105    public checkStatus(address: string) {
106      return fetch(`${this.url}/txStat/${address}`)
107        .then(checkHttpStatus)
108        .then(parseJSON);
109    }
110  
111    public sendAmount(
112      withdrawal: string,
113      originKind: string,
114      destinationKind: string,
115      destinationAmount: number
116    ) {
117      const pair = `${originKind.toLowerCase()}_${destinationKind.toLowerCase()}`;
118  
119      return fetch(`${this.url}/sendamount`, {
120        method: 'POST',
121        body: JSON.stringify({
122          amount: destinationAmount,
123          pair,
124          apiKey: this.apiKey,
125          withdrawal
126        }),
127        headers: new Headers(this.postHeaders)
128      })
129        .then(checkHttpStatus)
130        .then(parseJSON)
131        .catch(err => {
132          // CORS rejection, meaning metamask don't want us
133          if (err.name === 'TypeError') {
134            throw new Error(
135              'Shapeshift has blocked this request, visit shapeshift.io for more information or contact support'
136            );
137          }
138        });
139    }
140  
141    public getCoins() {
142      return fetch(`${this.url}/getcoins`)
143        .then(checkHttpStatus)
144        .then(parseJSON);
145    }
146  
147    public getAllRates = async () => {
148      const marketInfo = await this.getMarketInfo();
149      const pairRates = this.filterPairs(marketInfo);
150      const checkAvl = await this.checkAvl(pairRates);
151      const mappedRates = this.mapMarketInfo(checkAvl);
152      const allRates = this.addUnavailableCoinsAndTokens(mappedRates);
153  
154      return allRates;
155    };
156  
157    public addUnavailableCoinsAndTokens = (availableCoinsAndTokens: TokenMap) => {
158      if (this.fetchedSupportedCoinsAndTokens) {
159        /** @desc Create a hash for efficiently checking which tokens are currently available. */
160        const allOptions = flatten(
161          Object.values(availableCoinsAndTokens).map(({ options }) => options)
162        );
163        const availableOptions: ShapeshiftOptionMap = uniqBy(allOptions, 'id').reduce(
164          (prev: ShapeshiftOptionMap, next) => {
165            prev[next.id] = next;
166            return prev;
167          },
168          {}
169        );
170  
171        const unavailableCoinsAndTokens = this.whitelist
172          .map(token => {
173            /** @desc ShapeShift claims support for the token and it is available. */
174            const availableCoinOrToken = availableOptions[token];
175  
176            if (availableCoinOrToken) {
177              return null;
178            }
179  
180            /** @desc ShapeShift claims support for the token, but it is unavailable. */
181            const supportedCoinOrToken = this.supportedCoinsAndTokens[token];
182  
183            if (supportedCoinOrToken) {
184              const { symbol: id, image, name, status } = supportedCoinOrToken;
185  
186              return {
187                /** @desc Preface the false id with '__' to differentiate from actual pairs. */
188                id: `__${id}`,
189                limit: 0,
190                min: 0,
191                options: [{ id, image, name, status }]
192              };
193            }
194  
195            /** @desc We claim support for the coin or token, but ShapeShift doesn't. */
196            return null;
197          })
198          .reduce((prev: ShapeshiftOptionMap, next) => {
199            if (next) {
200              prev[next.id] = next;
201  
202              return prev;
203            }
204  
205            return prev;
206          }, {});
207  
208        return { ...availableCoinsAndTokens, ...unavailableCoinsAndTokens };
209      }
210  
211      return availableCoinsAndTokens;
212    };
213  
214    private filterPairs(marketInfo: ShapeshiftMarketInfo[]) {
215      return marketInfo.filter(obj => {
216        const { pair } = obj;
217        const pairArr = pair.split('_');
218        return this.whitelist.includes(pairArr[0]) && this.whitelist.includes(pairArr[1]);
219      });
220    }
221  
222    private async checkAvl(pairRates: IPairData[]) {
223      const avlCoins = await this.getAvlCoins();
224      const mapAvl = pairRates.map(p => {
225        const { pair } = p;
226        const pairArr = pair.split('_');
227  
228        if (pairArr[0] in avlCoins && pairArr[1] in avlCoins) {
229          return {
230            ...p,
231            ...{
232              [pairArr[0]]: {
233                name: avlCoins[pairArr[0]].name,
234                status: avlCoins[pairArr[0]].status,
235                image: avlCoins[pairArr[0]].image
236              },
237              [pairArr[1]]: {
238                name: avlCoins[pairArr[1]].name,
239                status: avlCoins[pairArr[1]].status,
240                image: avlCoins[pairArr[1]].image
241              }
242            }
243          };
244        }
245      });
246      const filered = mapAvl.filter(v => v);
247      return filered as (IPairData & IAvailablePairData)[];
248    }
249  
250    private getAvlCoins() {
251      return fetch(`${this.url}/getcoins`)
252        .then(checkHttpStatus)
253        .then(parseJSON)
254        .then(supportedCoinsAndTokens => {
255          this.supportedCoinsAndTokens = supportedCoinsAndTokens;
256          this.fetchedSupportedCoinsAndTokens = true;
257  
258          return supportedCoinsAndTokens;
259        });
260    }
261  
262    private getMarketInfo() {
263      return fetch(`${this.url}/marketinfo`)
264        .then(checkHttpStatus)
265        .then(parseJSON);
266    }
267  
268    private isWhitelisted(coin: string) {
269      return this.whitelist.includes(coin);
270    }
271  
272    private mapMarketInfo(marketInfo: (IPairData & IAvailablePairData)[]) {
273      const tokenMap: TokenMap = {};
274      marketInfo.forEach(m => {
275        const [originKind, destinationKind] = m.pair.split('_');
276        if (this.isWhitelisted(originKind) && this.isWhitelisted(destinationKind)) {
277          const pairName = originKind + destinationKind;
278          const { rate, limit, min } = m;
279          tokenMap[pairName] = {
280            id: pairName,
281            options: [
282              {
283                id: originKind,
284                status: m[originKind].status,
285                image: m[originKind].image,
286                name: m[originKind].name
287              },
288              {
289                id: destinationKind,
290                status: m[destinationKind].status,
291                image: m[destinationKind].image,
292                name: m[destinationKind].name
293              }
294            ],
295            rate,
296            limit,
297            min
298          };
299        }
300      });
301      return tokenMap;
302    }
303  }
304  
305  const shapeshift = new ShapeshiftService();
306  
307  export default shapeshift;