From 9d64adc10a30145d2e6e25927eb67c3ffd6289b1 Mon Sep 17 00:00:00 2001 From: Gergely Szilagyi Date: Fri, 9 Oct 2020 14:06:51 +0700 Subject: [PATCH 1/2] Add SideShift.ai exchange plugin --- src/index.js | 2 + src/swap/sideshift.js | 314 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 316 insertions(+) create mode 100644 src/swap/sideshift.js diff --git a/src/index.js b/src/index.js index c3ef6f16..bb270033 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ // @flow +import { makeSideshiftPlugin } from '../lib/swap/sideshift' import { makeBitMaxPlugin } from './rate/bitmax.js' import { makeCoinbasePlugin } from './rate/coinbase.js' import { makeCoincapPlugin } from './rate/coincap.js' @@ -42,6 +43,7 @@ const edgeCorePlugins = { foxExchange: makeFoxExchangePlugin, godex: makeGodexPlugin, shapeshift: makeShapeshiftPlugin, + sideshift: makeSideshiftPlugin, switchain: makeSwitchainPlugin, totle: makeTotlePlugin, transfer: makeTransferPlugin diff --git a/src/swap/sideshift.js b/src/swap/sideshift.js new file mode 100644 index 00000000..de9ccf1a --- /dev/null +++ b/src/swap/sideshift.js @@ -0,0 +1,314 @@ +// @flow + +import { + SwapAboveLimitError, + SwapBelowLimitError, + SwapPermissionError +} from 'edge-core-js' +import { + type EdgeCorePluginOptions, + type EdgeCurrencyWallet, + type EdgeFetchFunction, + type EdgeSpendInfo, + type EdgeSwapInfo, + type EdgeSwapPlugin, + type EdgeSwapQuote, + type EdgeSwapRequest, + SwapCurrencyError +} from 'edge-core-js/types' + +import { makeSwapPluginQuote } from '../swap-helpers.js' + +// Invalid currency codes should *not* have transcribed codes +// because currency codes with transcribed versions are NOT invalid +const CURRENCY_CODE_TRANSCRIPTION = { + // Edge currencyCode: exchangeCurrencyCode + USDT: 'usdtErc20' +} +const SIDESHIFT_BASE_URL = 'https://sideshift.ai/api/v1' +const pluginId = 'sideshift' +const swapInfo: EdgeSwapInfo = { + pluginId, + displayName: 'SideShift.ai', + supportEmail: 'help@sideshift.ai' +} + +type FixedQuote = { + createdAt: string, + depositAmount: string, + depositMethod: string, + expiresAt: string, + id: string, + rate: string, + settleAmount: string, + settleMethod: string, + error?: { message: string } +} + +type FixedQuoteRequest = { + depositMethod: string, + settleMethod: string, + depositAmount: string +} + +type Order = { + createdAt: string, + createdAtISO: string, + expiresAt: string, + expiresAtISO: string, + depositAddress: { + address: string + }, + depositMethod: string, + id: string, + orderId: string, + settleAddress: { + address: string + }, + settleMethod: string, + depositMax: string, + depositMin: string, + quoteId: string, + settleAmount: string, + depositAmount: string, + deposits: Array +} + +type OrderRequest = { + type: string, + quoteId: string, + affiliateId: string, + sessionSecret?: string, + settleAddress: string +} + +type Rate = { + rate: number, + min: string, + max: string, + error?: { + message: string + } +} + +type Permissions = { + createOrder: boolean, + createQuote: boolean +} + +const dontUseLegacy = {} + +async function getAddress( + wallet: EdgeCurrencyWallet, + currencyCode: string +): Promise { + const addressInfo = await wallet.getReceiveAddress({ currencyCode }) + return addressInfo.legacyAddress && !dontUseLegacy[currencyCode] + ? addressInfo.legacyAddress + : addressInfo.publicAddress +} + +function getSafeCurrencyCode(request: EdgeSwapRequest) { + const { fromCurrencyCode, toCurrencyCode } = request + + const safeFromCurrencyCode = + CURRENCY_CODE_TRANSCRIPTION[fromCurrencyCode] || + fromCurrencyCode.toLowerCase() + + const safeToCurrencyCode = + CURRENCY_CODE_TRANSCRIPTION[toCurrencyCode] || toCurrencyCode.toLowerCase() + + return { safeFromCurrencyCode, safeToCurrencyCode } +} + +async function checkQuoteError( + rate: Rate, + request: EdgeSwapRequest, + quoteErrorMessage: string +) { + const { fromCurrencyCode, fromWallet } = request + + const nativeMin = await fromWallet.denominationToNative( + rate.min, + fromCurrencyCode + ) + + const nativeMax = await fromWallet.denominationToNative( + rate.max, + fromCurrencyCode + ) + + if (quoteErrorMessage === 'Amount too low') { + throw new SwapBelowLimitError(swapInfo, nativeMin) + } + + if (quoteErrorMessage === 'Amount too high') { + throw new SwapAboveLimitError(swapInfo, nativeMax) + } +} + +export function makeSideshiftPlugin( + opts: EdgeCorePluginOptions +): EdgeSwapPlugin { + const { io, initOptions } = opts + + const api = createSideshiftApi(SIDESHIFT_BASE_URL, io.fetchCors || io.fetch) + + async function fetchSwapQuote( + request: EdgeSwapRequest + ): Promise { + const permissions = await api.get('/permissions') + + if (!permissions.createOrder || !permissions.createQuote) { + throw new SwapPermissionError(swapInfo, 'geoRestriction') + } + + const [depositAddress, settleAddress] = await Promise.all([ + getAddress(request.fromWallet, request.fromCurrencyCode), + getAddress(request.toWallet, request.toCurrencyCode) + ]) + + const { + safeFromCurrencyCode, + safeToCurrencyCode + } = await getSafeCurrencyCode(request) + + const rate = await api.get( + `/pairs/${safeFromCurrencyCode}/${safeToCurrencyCode}` + ) + + if (rate.error) { + throw new SwapCurrencyError( + swapInfo, + request.fromCurrencyCode, + request.toCurrencyCode + ) + } + + const quoteAmount = await (request.quoteFor === 'from' + ? request.fromWallet.nativeToDenomination( + request.nativeAmount, + request.fromCurrencyCode + ) + : request.toWallet.nativeToDenomination( + request.nativeAmount, + request.toCurrencyCode + )) + + const depositAmount = + request.quoteFor === 'from' + ? quoteAmount + : (parseFloat(quoteAmount) / rate.rate).toFixed(8).toString() + + const fixedQuoteRequest: FixedQuoteRequest = { + depositMethod: safeFromCurrencyCode, + settleMethod: safeToCurrencyCode, + depositAmount + } + + const fixedQuote = await api.post('/quotes', fixedQuoteRequest) + + if (fixedQuote.error) { + await checkQuoteError(rate, request, fixedQuote.error.message) + } + + const orderRequest: OrderRequest = { + type: 'fixed', + quoteId: fixedQuote.id, + affiliateId: initOptions.affiliateId, + settleAddress + } + + const order = await api.post('/orders', orderRequest) + + const spendInfoAmount = await request.fromWallet.denominationToNative( + order.depositAmount, + request.fromCurrencyCode.toUpperCase() + ) + + const amountExpectedFromNative = await request.fromWallet.denominationToNative( + order.depositAmount, + request.fromCurrencyCode + ) + + const amountExpectedToNative = await request.fromWallet.denominationToNative( + order.settleAmount, + request.toCurrencyCode + ) + + const isEstimate = false + + const spendInfo: EdgeSpendInfo = { + currencyCode: request.fromCurrencyCode, + spendTargets: [ + { + nativeAmount: spendInfoAmount, + publicAddress: order.depositAddress.address + } + ], + networkFeeOption: + request.fromCurrencyCode.toUpperCase() === 'BTC' ? 'high' : 'standard', + swapData: { + orderId: order.orderId, + isEstimate, + payoutAddress: settleAddress, + payoutCurrencyCode: safeToCurrencyCode, + payoutNativeAmount: amountExpectedToNative, + payoutWalletId: request.toWallet.id, + plugin: { ...swapInfo }, + refundAddress: depositAddress + } + } + + const tx = await request.fromWallet.makeSpend(spendInfo) + + return makeSwapPluginQuote( + request, + amountExpectedFromNative, + amountExpectedToNative, + tx, + settleAddress, + pluginId, + isEstimate, + new Date(order.expiresAtISO), + order.id + ) + } + + return { + swapInfo, + fetchSwapQuote + } +} + +function createSideshiftApi(baseUrl: string, fetch: EdgeFetchFunction) { + async function request( + method: 'GET' | 'POST', + path: string, + body: ?{} + ): Promise { + const url = `${baseUrl}${path}` + + const reply = await (method === 'GET' + ? fetch(url) + : fetch(url, { + method, + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + })) + + try { + return await reply.json() + } catch (e) { + throw new Error(`SideShift.ai returned error code ${reply.status}`) + } + } + + return { + get: (path: string): Promise => request('GET', path), + post: (path: string, body: {}): Promise => + request('POST', path, body) + } +} From f9f92db0d6398a29b8636151a7208862e7a7286b Mon Sep 17 00:00:00 2001 From: Gergely Szilagyi Date: Mon, 14 Dec 2020 22:27:06 +0100 Subject: [PATCH 2/2] Make requested changes --- src/swap/sideshift.js | 267 ++++++++++++++++++++++-------------------- 1 file changed, 142 insertions(+), 125 deletions(-) diff --git a/src/swap/sideshift.js b/src/swap/sideshift.js index de9ccf1a..75ec46db 100644 --- a/src/swap/sideshift.js +++ b/src/swap/sideshift.js @@ -1,5 +1,6 @@ // @flow +import { asBoolean, asNumber, asObject, asOptional, asString } from 'cleaners' import { SwapAboveLimitError, SwapBelowLimitError, @@ -33,79 +34,12 @@ const swapInfo: EdgeSwapInfo = { supportEmail: 'help@sideshift.ai' } -type FixedQuote = { - createdAt: string, - depositAmount: string, - depositMethod: string, - expiresAt: string, - id: string, - rate: string, - settleAmount: string, - settleMethod: string, - error?: { message: string } -} - -type FixedQuoteRequest = { - depositMethod: string, - settleMethod: string, - depositAmount: string -} - -type Order = { - createdAt: string, - createdAtISO: string, - expiresAt: string, - expiresAtISO: string, - depositAddress: { - address: string - }, - depositMethod: string, - id: string, - orderId: string, - settleAddress: { - address: string - }, - settleMethod: string, - depositMax: string, - depositMin: string, - quoteId: string, - settleAmount: string, - depositAmount: string, - deposits: Array -} - -type OrderRequest = { - type: string, - quoteId: string, - affiliateId: string, - sessionSecret?: string, - settleAddress: string -} - -type Rate = { - rate: number, - min: string, - max: string, - error?: { - message: string - } -} - -type Permissions = { - createOrder: boolean, - createQuote: boolean -} - -const dontUseLegacy = {} - async function getAddress( wallet: EdgeCurrencyWallet, currencyCode: string ): Promise { const addressInfo = await wallet.getReceiveAddress({ currencyCode }) - return addressInfo.legacyAddress && !dontUseLegacy[currencyCode] - ? addressInfo.legacyAddress - : addressInfo.publicAddress + return addressInfo.segwitAddress ?? addressInfo.publicAddress } function getSafeCurrencyCode(request: EdgeSwapRequest) { @@ -128,36 +62,60 @@ async function checkQuoteError( ) { const { fromCurrencyCode, fromWallet } = request - const nativeMin = await fromWallet.denominationToNative( - rate.min, - fromCurrencyCode - ) - - const nativeMax = await fromWallet.denominationToNative( - rate.max, - fromCurrencyCode - ) - if (quoteErrorMessage === 'Amount too low') { + const nativeMin = await fromWallet.denominationToNative( + rate.min, + fromCurrencyCode + ) throw new SwapBelowLimitError(swapInfo, nativeMin) } if (quoteErrorMessage === 'Amount too high') { + const nativeMax = await fromWallet.denominationToNative( + rate.max, + fromCurrencyCode + ) throw new SwapAboveLimitError(swapInfo, nativeMax) } } -export function makeSideshiftPlugin( - opts: EdgeCorePluginOptions -): EdgeSwapPlugin { - const { io, initOptions } = opts +const createSideshiftApi = (baseUrl: string, fetch: EdgeFetchFunction) => { + async function request( + method: 'GET' | 'POST', + path: string, + body: ?{} + ): Promise { + const url = `${baseUrl}${path}` - const api = createSideshiftApi(SIDESHIFT_BASE_URL, io.fetchCors || io.fetch) + const reply = await (method === 'GET' + ? fetch(url) + : fetch(url, { + method, + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + })) + + try { + return await reply.json() + } catch (e) { + throw new Error(`SideShift.ai returned error code ${reply.status}`) + } + } + + return { + get: (path: string): Promise => request('GET', path), + post: (path: string, body: {}): Promise => + request('POST', path, body) + } +} +const createFetchSwapQuote = (api: SideshiftApi, affiliateId: string) => async function fetchSwapQuote( request: EdgeSwapRequest ): Promise { - const permissions = await api.get('/permissions') + const permissions = asPermissions(await api.get('/permissions')) if (!permissions.createOrder || !permissions.createQuote) { throw new SwapPermissionError(swapInfo, 'geoRestriction') @@ -168,13 +126,14 @@ export function makeSideshiftPlugin( getAddress(request.toWallet, request.toCurrencyCode) ]) - const { - safeFromCurrencyCode, - safeToCurrencyCode - } = await getSafeCurrencyCode(request) + const { safeFromCurrencyCode, safeToCurrencyCode } = getSafeCurrencyCode( + request + ) - const rate = await api.get( - `/pairs/${safeFromCurrencyCode}/${safeToCurrencyCode}` + const rate = asRate( + await api.get( + `/pairs/${safeFromCurrencyCode}/${safeToCurrencyCode}` + ) ) if (rate.error) { @@ -200,26 +159,30 @@ export function makeSideshiftPlugin( ? quoteAmount : (parseFloat(quoteAmount) / rate.rate).toFixed(8).toString() - const fixedQuoteRequest: FixedQuoteRequest = { + const fixedQuoteRequest = asFixedQuoteRequest({ depositMethod: safeFromCurrencyCode, settleMethod: safeToCurrencyCode, depositAmount - } + }) - const fixedQuote = await api.post('/quotes', fixedQuoteRequest) + const fixedQuote = asFixedQuote( + await api.post('/quotes', fixedQuoteRequest) + ) if (fixedQuote.error) { await checkQuoteError(rate, request, fixedQuote.error.message) } - const orderRequest: OrderRequest = { + const orderRequest = asOrderRequest({ type: 'fixed', quoteId: fixedQuote.id, - affiliateId: initOptions.affiliateId, + affiliateId, settleAddress - } + }) - const order = await api.post('/orders', orderRequest) + const order = asOrder( + await api.post('/orders', orderRequest) + ) const spendInfoAmount = await request.fromWallet.denominationToNative( order.depositAmount, @@ -275,40 +238,94 @@ export function makeSideshiftPlugin( ) } +export function makeSideshiftPlugin( + opts: EdgeCorePluginOptions +): EdgeSwapPlugin { + const { io, initOptions } = opts + + const api = createSideshiftApi(SIDESHIFT_BASE_URL, io.fetchCors || io.fetch) + + const fetchSwapQuote = createFetchSwapQuote(api, initOptions.affiliateId) + return { swapInfo, fetchSwapQuote } } -function createSideshiftApi(baseUrl: string, fetch: EdgeFetchFunction) { - async function request( - method: 'GET' | 'POST', - path: string, - body: ?{} - ): Promise { - const url = `${baseUrl}${path}` - - const reply = await (method === 'GET' - ? fetch(url) - : fetch(url, { - method, - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(body) - })) +interface SideshiftApi { + get: (path: string) => Promise; + post: (path: string, body: {}) => Promise; +} - try { - return await reply.json() - } catch (e) { - throw new Error(`SideShift.ai returned error code ${reply.status}`) - } - } +interface Permission { + createOrder: boolean; + createQuote: boolean; +} - return { - get: (path: string): Promise => request('GET', path), - post: (path: string, body: {}): Promise => - request('POST', path, body) - } +interface Rate { + rate: number; + min: string; + max: string; + error: { message: string } | typeof undefined; } + +const asPermissions = asObject({ + createOrder: asBoolean, + createQuote: asBoolean +}) + +const asRate = asObject({ + rate: asNumber, + min: asString, + max: asString, + error: asOptional(asObject({ message: asString })) +}) + +const asFixedQuoteRequest = asObject({ + depositMethod: asString, + settleMethod: asString, + depositAmount: asString +}) + +const asFixedQuote = asObject({ + createdAt: asString, + depositAmount: asString, + depositMethod: asString, + expiresAt: asString, + id: asString, + rate: asString, + settleAmount: asString, + settleMethod: asString, + error: asOptional(asObject({ message: asString })) +}) + +const asOrderRequest = asObject({ + type: asString, + quoteId: asString, + affiliateId: asString, + sessionSecret: asOptional(asString), + settleAddress: asString +}) + +const asOrder = asObject({ + createdAt: asString, + createdAtISO: asString, + expiresAt: asString, + expiresAtISO: asString, + depositAddress: asObject({ + address: asString + }), + depositMethod: asString, + id: asString, + orderId: asString, + settleAddress: asObject({ + address: asString + }), + settleMethod: asString, + depositMax: asString, + depositMin: asString, + quoteId: asString, + settleAmount: asString, + depositAmount: asString +})