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;