Skip to content

Commit 2c4a38b

Browse files
authored
Merge pull request #100 from sideshift/master
Add SideShift.ai exchange plugin
2 parents 7b5020d + f9f92db commit 2c4a38b

2 files changed

Lines changed: 333 additions & 0 deletions

File tree

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// @flow
22

3+
import { makeSideshiftPlugin } from '../lib/swap/sideshift'
34
import { makeBitMaxPlugin } from './rate/bitmax.js'
45
import { makeCoinbasePlugin } from './rate/coinbase.js'
56
import { makeCoincapPlugin } from './rate/coincap.js'
@@ -42,6 +43,7 @@ const edgeCorePlugins = {
4243
foxExchange: makeFoxExchangePlugin,
4344
godex: makeGodexPlugin,
4445
shapeshift: makeShapeshiftPlugin,
46+
sideshift: makeSideshiftPlugin,
4547
switchain: makeSwitchainPlugin,
4648
totle: makeTotlePlugin,
4749
transfer: makeTransferPlugin

src/swap/sideshift.js

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
// @flow
2+
3+
import { asBoolean, asNumber, asObject, asOptional, asString } from 'cleaners'
4+
import {
5+
SwapAboveLimitError,
6+
SwapBelowLimitError,
7+
SwapPermissionError
8+
} from 'edge-core-js'
9+
import {
10+
type EdgeCorePluginOptions,
11+
type EdgeCurrencyWallet,
12+
type EdgeFetchFunction,
13+
type EdgeSpendInfo,
14+
type EdgeSwapInfo,
15+
type EdgeSwapPlugin,
16+
type EdgeSwapQuote,
17+
type EdgeSwapRequest,
18+
SwapCurrencyError
19+
} from 'edge-core-js/types'
20+
21+
import { makeSwapPluginQuote } from '../swap-helpers.js'
22+
23+
// Invalid currency codes should *not* have transcribed codes
24+
// because currency codes with transcribed versions are NOT invalid
25+
const CURRENCY_CODE_TRANSCRIPTION = {
26+
// Edge currencyCode: exchangeCurrencyCode
27+
USDT: 'usdtErc20'
28+
}
29+
const SIDESHIFT_BASE_URL = 'https://sideshift.ai/api/v1'
30+
const pluginId = 'sideshift'
31+
const swapInfo: EdgeSwapInfo = {
32+
pluginId,
33+
displayName: 'SideShift.ai',
34+
supportEmail: 'help@sideshift.ai'
35+
}
36+
37+
async function getAddress(
38+
wallet: EdgeCurrencyWallet,
39+
currencyCode: string
40+
): Promise<string> {
41+
const addressInfo = await wallet.getReceiveAddress({ currencyCode })
42+
return addressInfo.segwitAddress ?? addressInfo.publicAddress
43+
}
44+
45+
function getSafeCurrencyCode(request: EdgeSwapRequest) {
46+
const { fromCurrencyCode, toCurrencyCode } = request
47+
48+
const safeFromCurrencyCode =
49+
CURRENCY_CODE_TRANSCRIPTION[fromCurrencyCode] ||
50+
fromCurrencyCode.toLowerCase()
51+
52+
const safeToCurrencyCode =
53+
CURRENCY_CODE_TRANSCRIPTION[toCurrencyCode] || toCurrencyCode.toLowerCase()
54+
55+
return { safeFromCurrencyCode, safeToCurrencyCode }
56+
}
57+
58+
async function checkQuoteError(
59+
rate: Rate,
60+
request: EdgeSwapRequest,
61+
quoteErrorMessage: string
62+
) {
63+
const { fromCurrencyCode, fromWallet } = request
64+
65+
if (quoteErrorMessage === 'Amount too low') {
66+
const nativeMin = await fromWallet.denominationToNative(
67+
rate.min,
68+
fromCurrencyCode
69+
)
70+
throw new SwapBelowLimitError(swapInfo, nativeMin)
71+
}
72+
73+
if (quoteErrorMessage === 'Amount too high') {
74+
const nativeMax = await fromWallet.denominationToNative(
75+
rate.max,
76+
fromCurrencyCode
77+
)
78+
throw new SwapAboveLimitError(swapInfo, nativeMax)
79+
}
80+
}
81+
82+
const createSideshiftApi = (baseUrl: string, fetch: EdgeFetchFunction) => {
83+
async function request<R>(
84+
method: 'GET' | 'POST',
85+
path: string,
86+
body: ?{}
87+
): Promise<R> {
88+
const url = `${baseUrl}${path}`
89+
90+
const reply = await (method === 'GET'
91+
? fetch(url)
92+
: fetch(url, {
93+
method,
94+
headers: {
95+
'Content-Type': 'application/json'
96+
},
97+
body: JSON.stringify(body)
98+
}))
99+
100+
try {
101+
return await reply.json()
102+
} catch (e) {
103+
throw new Error(`SideShift.ai returned error code ${reply.status}`)
104+
}
105+
}
106+
107+
return {
108+
get: <R>(path: string): Promise<R> => request<R>('GET', path),
109+
post: <R>(path: string, body: {}): Promise<R> =>
110+
request<R>('POST', path, body)
111+
}
112+
}
113+
114+
const createFetchSwapQuote = (api: SideshiftApi, affiliateId: string) =>
115+
async function fetchSwapQuote(
116+
request: EdgeSwapRequest
117+
): Promise<EdgeSwapQuote> {
118+
const permissions = asPermissions(await api.get<Permission>('/permissions'))
119+
120+
if (!permissions.createOrder || !permissions.createQuote) {
121+
throw new SwapPermissionError(swapInfo, 'geoRestriction')
122+
}
123+
124+
const [depositAddress, settleAddress] = await Promise.all([
125+
getAddress(request.fromWallet, request.fromCurrencyCode),
126+
getAddress(request.toWallet, request.toCurrencyCode)
127+
])
128+
129+
const { safeFromCurrencyCode, safeToCurrencyCode } = getSafeCurrencyCode(
130+
request
131+
)
132+
133+
const rate = asRate(
134+
await api.get<typeof asRate>(
135+
`/pairs/${safeFromCurrencyCode}/${safeToCurrencyCode}`
136+
)
137+
)
138+
139+
if (rate.error) {
140+
throw new SwapCurrencyError(
141+
swapInfo,
142+
request.fromCurrencyCode,
143+
request.toCurrencyCode
144+
)
145+
}
146+
147+
const quoteAmount = await (request.quoteFor === 'from'
148+
? request.fromWallet.nativeToDenomination(
149+
request.nativeAmount,
150+
request.fromCurrencyCode
151+
)
152+
: request.toWallet.nativeToDenomination(
153+
request.nativeAmount,
154+
request.toCurrencyCode
155+
))
156+
157+
const depositAmount =
158+
request.quoteFor === 'from'
159+
? quoteAmount
160+
: (parseFloat(quoteAmount) / rate.rate).toFixed(8).toString()
161+
162+
const fixedQuoteRequest = asFixedQuoteRequest({
163+
depositMethod: safeFromCurrencyCode,
164+
settleMethod: safeToCurrencyCode,
165+
depositAmount
166+
})
167+
168+
const fixedQuote = asFixedQuote(
169+
await api.post<typeof asFixedQuote>('/quotes', fixedQuoteRequest)
170+
)
171+
172+
if (fixedQuote.error) {
173+
await checkQuoteError(rate, request, fixedQuote.error.message)
174+
}
175+
176+
const orderRequest = asOrderRequest({
177+
type: 'fixed',
178+
quoteId: fixedQuote.id,
179+
affiliateId,
180+
settleAddress
181+
})
182+
183+
const order = asOrder(
184+
await api.post<typeof asOrder>('/orders', orderRequest)
185+
)
186+
187+
const spendInfoAmount = await request.fromWallet.denominationToNative(
188+
order.depositAmount,
189+
request.fromCurrencyCode.toUpperCase()
190+
)
191+
192+
const amountExpectedFromNative = await request.fromWallet.denominationToNative(
193+
order.depositAmount,
194+
request.fromCurrencyCode
195+
)
196+
197+
const amountExpectedToNative = await request.fromWallet.denominationToNative(
198+
order.settleAmount,
199+
request.toCurrencyCode
200+
)
201+
202+
const isEstimate = false
203+
204+
const spendInfo: EdgeSpendInfo = {
205+
currencyCode: request.fromCurrencyCode,
206+
spendTargets: [
207+
{
208+
nativeAmount: spendInfoAmount,
209+
publicAddress: order.depositAddress.address
210+
}
211+
],
212+
networkFeeOption:
213+
request.fromCurrencyCode.toUpperCase() === 'BTC' ? 'high' : 'standard',
214+
swapData: {
215+
orderId: order.orderId,
216+
isEstimate,
217+
payoutAddress: settleAddress,
218+
payoutCurrencyCode: safeToCurrencyCode,
219+
payoutNativeAmount: amountExpectedToNative,
220+
payoutWalletId: request.toWallet.id,
221+
plugin: { ...swapInfo },
222+
refundAddress: depositAddress
223+
}
224+
}
225+
226+
const tx = await request.fromWallet.makeSpend(spendInfo)
227+
228+
return makeSwapPluginQuote(
229+
request,
230+
amountExpectedFromNative,
231+
amountExpectedToNative,
232+
tx,
233+
settleAddress,
234+
pluginId,
235+
isEstimate,
236+
new Date(order.expiresAtISO),
237+
order.id
238+
)
239+
}
240+
241+
export function makeSideshiftPlugin(
242+
opts: EdgeCorePluginOptions
243+
): EdgeSwapPlugin {
244+
const { io, initOptions } = opts
245+
246+
const api = createSideshiftApi(SIDESHIFT_BASE_URL, io.fetchCors || io.fetch)
247+
248+
const fetchSwapQuote = createFetchSwapQuote(api, initOptions.affiliateId)
249+
250+
return {
251+
swapInfo,
252+
fetchSwapQuote
253+
}
254+
}
255+
256+
interface SideshiftApi {
257+
get: <R>(path: string) => Promise<R>;
258+
post: <R>(path: string, body: {}) => Promise<R>;
259+
}
260+
261+
interface Permission {
262+
createOrder: boolean;
263+
createQuote: boolean;
264+
}
265+
266+
interface Rate {
267+
rate: number;
268+
min: string;
269+
max: string;
270+
error: { message: string } | typeof undefined;
271+
}
272+
273+
const asPermissions = asObject({
274+
createOrder: asBoolean,
275+
createQuote: asBoolean
276+
})
277+
278+
const asRate = asObject({
279+
rate: asNumber,
280+
min: asString,
281+
max: asString,
282+
error: asOptional(asObject({ message: asString }))
283+
})
284+
285+
const asFixedQuoteRequest = asObject({
286+
depositMethod: asString,
287+
settleMethod: asString,
288+
depositAmount: asString
289+
})
290+
291+
const asFixedQuote = asObject({
292+
createdAt: asString,
293+
depositAmount: asString,
294+
depositMethod: asString,
295+
expiresAt: asString,
296+
id: asString,
297+
rate: asString,
298+
settleAmount: asString,
299+
settleMethod: asString,
300+
error: asOptional(asObject({ message: asString }))
301+
})
302+
303+
const asOrderRequest = asObject({
304+
type: asString,
305+
quoteId: asString,
306+
affiliateId: asString,
307+
sessionSecret: asOptional(asString),
308+
settleAddress: asString
309+
})
310+
311+
const asOrder = asObject({
312+
createdAt: asString,
313+
createdAtISO: asString,
314+
expiresAt: asString,
315+
expiresAtISO: asString,
316+
depositAddress: asObject({
317+
address: asString
318+
}),
319+
depositMethod: asString,
320+
id: asString,
321+
orderId: asString,
322+
settleAddress: asObject({
323+
address: asString
324+
}),
325+
settleMethod: asString,
326+
depositMax: asString,
327+
depositMin: asString,
328+
quoteId: asString,
329+
settleAmount: asString,
330+
depositAmount: asString
331+
})

0 commit comments

Comments
 (0)