Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add api2 criptointercambio #192

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { makeNomicsPlugin } from './rate/nomics.js'
import { makeWazirxPlugin } from './rate/wazirx'
import { makeChangellyPlugin } from './swap/changelly.js'
import { makeChangeNowPlugin } from './swap/changenow.js'
import { makeCriptointercambioPlugin } from './swap/criptointercambio.js'
import { makeSpookySwapPlugin } from './swap/defi/uni-v2-based/plugins/spookySwap.js'
import { makeTombSwapPlugin } from './swap/defi/uni-v2-based/plugins/tombSwap.js'
import { makeExolixPlugin } from './swap/exolix.js'
Expand Down Expand Up @@ -40,6 +41,7 @@ const edgeCorePlugins = {
// Swap plugins:
changelly: makeChangellyPlugin,
changenow: makeChangeNowPlugin,
criptointercambio: makeCriptointercambioPlugin,
exolix: makeExolixPlugin,
foxExchange: makeFoxExchangePlugin,
godex: makeGodexPlugin,
Expand Down
28 changes: 28 additions & 0 deletions src/swap-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,31 @@ export const getCodesWithMainnetTranscription = (
toCurrencyCode: toCurrencyCode
}
}

export const checkEthTokensOnly = (
swapInfo: EdgeSwapInfo,
request: EdgeSwapRequest
): void => {
const currencyFromWallet = request.fromWallet.currencyInfo.currencyCode
const currencyToWallet = request.toWallet.currencyInfo.currencyCode

if (
currencyFromWallet !== request.fromCurrencyCode &&
currencyFromWallet !== 'ETH'
) {
throw new SwapCurrencyError(
swapInfo,
request.fromCurrencyCode,
request.toCurrencyCode
)
} else if (
currencyToWallet !== request.toCurrencyCode &&
currencyToWallet !== 'ETH'
) {
throw new SwapCurrencyError(
swapInfo,
request.fromCurrencyCode,
request.toCurrencyCode
)
}
}
347 changes: 347 additions & 0 deletions src/swap/criptointercambio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,347 @@
// @flow

import {
type ObjectCleaner,
asBoolean,
asNumber,
asObject,
asOptional,
asString
} from 'cleaners'
import { type EdgeFetchFunction } from 'edge-core-js'
import {
type EdgeCorePluginOptions,
type EdgeCurrencyWallet,
type EdgeSpendInfo,
type EdgeSwapInfo,
type EdgeSwapPlugin,
type EdgeSwapQuote,
type EdgeSwapRequest,
type EdgeTransaction,
SwapAboveLimitError,
SwapBelowLimitError,
SwapCurrencyError
} from 'edge-core-js/types'
import hashjs from 'hash.js'
import { base16 } from 'rfc4648'
import utf8Codec from 'utf8'

import {
type InvalidCurrencyCodes,
checkEthTokensOnly,
checkInvalidCodes,
makeSwapPluginQuote,
safeCurrencyCodes
} from '../swap-helpers.js'

const INVALID_CURRENCY_CODES: InvalidCurrencyCodes = {
from: {
ethereum: ['BNB', 'FTM', 'MATIC', 'KNC'],
avalanche: 'allTokens',
binancesmartchain: 'allTokens',
polygon: 'allCodes',
celo: 'allTokens',
fantom: 'allCodes'
},
to: {
ethereum: ['BNB', 'FTM', 'MATIC', 'KNC'],
avalanche: 'allTokens',
binancesmartchain: 'allTokens',
polygon: 'allCodes',
celo: 'allTokens',
fantom: 'allCodes',
zcash: ['ZEC']
}
}

// Invalid currency codes should *not* have transcribed codes
// because currency codes with transcribed versions are NOT invalid
const CURRENCY_CODE_TRANSCRIPTION = {
ethereum: {
USDT: 'USDT20'
},
binancesmartchain: {
BNB: 'BNBBSC'
}
}

const pluginId = 'criptointercambio'
const swapInfo: EdgeSwapInfo = {
pluginId,
displayName: 'Criptointercambio',
supportEmail: 'support@criptointercambio.com'
}
const orderUri = 'https://criptointercambio.com/transaction/'
const uri = 'https://api2.criptointercambio.com'
const expirationFixedMs = 1000 * 60 * 5
const asRequestOptions = asObject({
apiKey: asString,
secret: asString
})
type RequestOptions = $Call<typeof asRequestOptions>
const asCreateFixedTransaction = asObject({
id: asString,
amountExpectedFrom: asString,
amountExpectedTo: asString,
amountTo: asNumber,
apiExtraFee: asString,
createdAt: asString,
currencyFrom: asString,
currencyTo: asString,
kycRequired: asBoolean,
payinAddress: asString,
payinExtraId: asOptional(asString),
payoutAddress: asString,
payoutExtraId: asOptional(asString),
refundAddress: asString,
refundExtraId: asOptional(asString),
status: asString
})
const asGetFixRateForAmount = asObject({
id: asString
})
const dontUseLegacy = {
DGB: true
}

function hmacSha512(data: Uint8Array, key: Uint8Array): Uint8Array {
const hmac = hashjs.hmac(hashjs.sha512, key)
return hmac.update(data).digest()
}

function parseUtf8(text: string): Uint8Array {
const byteString: string = utf8Codec.encode(text)
const out = new Uint8Array(byteString.length)

for (let i = 0; i < byteString.length; ++i) {
out[i] = byteString.charCodeAt(i)
}

return out
}

async function getAddress(
wallet: EdgeCurrencyWallet,
currencyCode: string
): Promise<string> {
const addressInfo = await wallet.getReceiveAddress({ currencyCode })
return addressInfo.legacyAddress && !dontUseLegacy[currencyCode]
? addressInfo.legacyAddress
: addressInfo.publicAddress
}

type FetcherType = (json: any, promoCode?: string) => Promise<any>

function makeFetcher(
fetch: EdgeFetchFunction,
options: RequestOptions
): FetcherType {
return async function (json: any, promoCode?: string) {
const body = JSON.stringify(json)
const sign = base16
.stringify(hmacSha512(parseUtf8(body), parseUtf8(options.secret)))
.toLowerCase()

const headers: { [header: string]: string } = {
'Content-Type': 'application/json',
'api-key': options.apiKey,
sign
}
if (promoCode != null) headers['X-Promo-Code'] = promoCode
const response = await fetch(uri, { method: 'POST', body, headers })

if (!response.ok) {
throw new Error(
`Criptointercambio returned error code ${response.status}`
)
}
return response.json()
}
}

async function wrapCleanerRequest(
cleaner: ObjectCleaner<any>,
fetcher: FetcherType,
request: EdgeSwapRequest,
method: string,
params: any
) {
const { promoCode, ...restParams } = params
const response = await fetcher(
{
jsonrpc: '2.0',
id: 'one',
method,
restParams
},
promoCode
)
await checkReply(response, request)
return cleaner(response.result)
}

async function checkReply(reply: Object, request: EdgeSwapRequest) {
const { fromCurrencyCode, fromWallet } = request
if (reply.error != null) {
if (
reply.error.code === -32602 ||
/Invalid currency:/.test(reply.error.message)
) {
throw new SwapCurrencyError(
swapInfo,
request.fromCurrencyCode,
request.toCurrencyCode
)
}
if (
reply.error.code === -32600 ||
/Invalid amout:/.test(reply.error.message)
) {
const matcher = reply.error.code.match(/([\d\\.]+)$/gim)
const minmaxAmount =
matcher.length > 0
? await fromWallet.denominationToNative(matcher[0], fromCurrencyCode)
: ''
if (/minimal amount/.test(reply.error.message)) {
throw new SwapBelowLimitError(swapInfo, minmaxAmount)
} else {
throw new SwapAboveLimitError(swapInfo, minmaxAmount)
}
}

throw new Error('Criptointercambio error: ' + JSON.stringify(reply.error))
}
}

export function makeCriptointercambioPlugin(
opts: EdgeCorePluginOptions
): EdgeSwapPlugin {
const { initOptions, io } = opts
const { fetchCors = io.fetch } = io

if (initOptions.apiKey == null || initOptions.secret == null) {
throw new Error('No Criptointercambio apiKey or secret provided.')
}
const fetcher = makeFetcher(fetchCors, asRequestOptions(initOptions))

return {
swapInfo,
async fetchSwapQuote(
request: EdgeSwapRequest,
userSettings: Object | void,
opts: { promoCode?: string }
): Promise<EdgeSwapQuote> {
checkInvalidCodes(INVALID_CURRENCY_CODES, request, swapInfo)
checkEthTokensOnly(swapInfo, request)

const fixedPromise = this.getFixedQuote(request, userSettings, opts)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just return here. No need for the const fixedPromise

// FIXME: Estimated swaps are temporarily disabled
const fixedResult = await fixedPromise
return fixedResult
},

async getFixedQuote(
request: EdgeSwapRequest,
userSettings: Object | void,
opts: { promoCode?: string }
): Promise<EdgeSwapQuote> {
const { promoCode } = opts
const [fromAddress, toAddress] = await Promise.all([
getAddress(request.fromWallet, request.fromCurrencyCode),
getAddress(request.toWallet, request.toCurrencyCode)
])
const quoteAmount =
request.quoteFor === 'from'
? await request.fromWallet.nativeToDenomination(
request.nativeAmount,
request.fromCurrencyCode
)
: await request.toWallet.nativeToDenomination(
request.nativeAmount,
request.toCurrencyCode
)

const { safeFromCurrencyCode, safeToCurrencyCode } = safeCurrencyCodes(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit concerning. Your native API should support specifying the chaincode separately from the token code. Without that, there could easily be confusion on what token the user wants to send or purchase.

The CURRENCY_CODE_TRANSCRIPTION is a quick hack for prior partners but all new partners should have an explicit chaincode and tokencode. See the Changenow plugin

https://github.com/EdgeApp/edge-exchange-plugins/blob/master/src/swap/changenow.js#L110

CURRENCY_CODE_TRANSCRIPTION,
request
)

const fixedRate = await wrapCleanerRequest(
asGetFixRateForAmount,
fetcher,
request,
'getFixRateForAmount',
{
from: safeFromCurrencyCode,
to: safeToCurrencyCode,
[request.quoteFor === 'from' ? 'amountFrom' : 'amountTo']: quoteAmount
}
)

const fixedTx = await wrapCleanerRequest(
asCreateFixedTransaction,
fetcher,
request,
'createFixTransaction',
{
[request.quoteFor === 'from' ? 'amount' : 'amountTo']: quoteAmount,
from: safeFromCurrencyCode,
to: safeToCurrencyCode,
address: toAddress,
extraId: null,
refundAddress: fromAddress,
refundExtraId: null,
rateId: fixedRate.result.id,
//
promoCode
}
)

const amountExpectedFromNative = await request.fromWallet.denominationToNative(
fixedTx.amountExpectedFrom,
request.fromCurrencyCode
)
const amountExpectedToNative = await request.toWallet.denominationToNative(
fixedTx.amountExpectedTo,
request.toCurrencyCode
)

const spendInfo: EdgeSpendInfo = {
currencyCode: request.fromCurrencyCode,
spendTargets: [
{
nativeAmount: amountExpectedFromNative,
publicAddress: fixedTx.payinAddress,
uniqueIdentifier: fixedTx.payinExtraId || undefined
}
],
networkFeeOption:
request.fromCurrencyCode === 'BTC' ? 'high' : 'standard',
swapData: {
orderId: fixedTx.id,
orderUri: orderUri + fixedTx.id,
isEstimate: false,
payoutAddress: toAddress,
payoutCurrencyCode: request.toCurrencyCode,
payoutNativeAmount: amountExpectedToNative,
payoutWalletId: request.toWallet.id,
plugin: { ...swapInfo },
refundAddress: fromAddress
}
}
const tx: EdgeTransaction = await request.fromWallet.makeSpend(spendInfo)

return makeSwapPluginQuote(
request,
amountExpectedFromNative,
amountExpectedToNative,
tx,
toAddress,
pluginId,
false,
new Date(Date.now() + expirationFixedMs),
fixedTx.id
)
}
}
}