-
Notifications
You must be signed in to change notification settings - Fork 49
update api #433
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
base: master
Are you sure you want to change the base?
update api #433
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import { gt, lt, mul } from 'biggystring' | ||
| import { gt, lt } from 'biggystring' | ||
| import { | ||
| asEither, | ||
| asMaybe, | ||
|
|
@@ -10,6 +10,7 @@ import { | |
| } from 'cleaners' | ||
| import { | ||
| EdgeCorePluginOptions, | ||
| EdgeCurrencyWallet, | ||
| EdgeFetchResponse, | ||
| EdgeMemo, | ||
| EdgeSpendInfo, | ||
|
|
@@ -23,12 +24,12 @@ import { | |
| } from 'edge-core-js/types' | ||
|
|
||
| import { exolix as exolixMapping } from '../../mappings/exolix' | ||
| import { div18 } from '../../util/biggystringplus' | ||
| import { EdgeCurrencyPluginId } from '../../util/edgeCurrencyPluginIds' | ||
| import { | ||
| checkInvalidTokenIds, | ||
| checkWhitelistedMainnetCodes, | ||
| CurrencyPluginIdSwapChainCodeMap, | ||
| getCodesWithTranscription, | ||
| getContractAddresses, | ||
| getMaxSwappable, | ||
| InvalidTokenIds, | ||
| makeSwapPluginQuote, | ||
|
|
@@ -38,21 +39,15 @@ import { | |
| import { | ||
| convertRequest, | ||
| denominationToNative, | ||
| fetchRates, | ||
| getAddress, | ||
| memoType, | ||
| nativeToDenomination | ||
| } from '../../util/utils' | ||
| import { | ||
| asRatesResponse, | ||
| EdgeSwapRequestPlugin, | ||
| RatesRespose, | ||
| StringMap | ||
| } from '../types' | ||
| import { EdgeSwapRequestPlugin, StringMap } from '../types' | ||
|
|
||
| const pluginId = 'exolix' | ||
|
|
||
| const swapInfo: EdgeSwapInfo = { | ||
| export const swapInfo: EdgeSwapInfo = { | ||
| pluginId, | ||
| isDex: false, | ||
| displayName: 'Exolix', | ||
|
|
@@ -63,7 +58,6 @@ const asInitOptions = asObject({ | |
| apiKey: asString | ||
| }) | ||
|
|
||
| const MAX_USD_VALUE = '70000' | ||
| const INVALID_TOKEN_IDS: InvalidTokenIds = { | ||
| from: { | ||
| polygon: [ | ||
|
|
@@ -79,6 +73,31 @@ const INVALID_TOKEN_IDS: InvalidTokenIds = { | |
| } | ||
| } | ||
|
|
||
| interface ExolixCommonQuoteParams { | ||
| networkFrom: string | ||
| networkTo: string | ||
| coinAddressFrom?: string | ||
| coinAddressTo?: string | ||
| networkFromChainId?: number | ||
| networkToChainId?: number | ||
| withdrawalAddress: string | ||
| withdrawalExtraId: string | ||
| refundAddress: string | ||
| refundExtraId: string | ||
| rateType: 'fixed' | 'float' | ||
| rateId?: string | ||
| } | ||
|
|
||
| type ExolixFromQuoteParams = ExolixCommonQuoteParams & { | ||
| amount: string | ||
| } | ||
|
|
||
| type ExolixToQuoteParams = ExolixCommonQuoteParams & { | ||
| withdrawalAmount: string | ||
| } | ||
|
|
||
| type ExolixQuoteParams = ExolixFromQuoteParams | ExolixToQuoteParams | ||
|
|
||
| const addressTypeMap: StringMap = { | ||
| digibyte: 'publicAddress', | ||
| zcash: 'transparentAddress' | ||
|
|
@@ -99,21 +118,35 @@ while true; do | |
| ((n++)); | ||
| done | ||
| */ | ||
| const MAINNET_CODE_TRANSCRIPTION: CurrencyPluginIdSwapChainCodeMap = mapToRecord( | ||
|
|
||
| const EVM_CHAIN_NETWORK = 'evmGeneric' | ||
|
|
||
| export const MAINNET_CODE_TRANSCRIPTION: CurrencyPluginIdSwapChainCodeMap = mapToRecord( | ||
| exolixMapping | ||
| ) | ||
|
|
||
| const getNetwork = (wallet: EdgeCurrencyWallet): string | null => { | ||
| const evmChainId = wallet.currencyInfo.evmChainId | ||
| if (evmChainId != null) return EVM_CHAIN_NETWORK | ||
| return MAINNET_CODE_TRANSCRIPTION[ | ||
| wallet.currencyInfo.pluginId as EdgeCurrencyPluginId | ||
| ] | ||
| } | ||
|
|
||
| const orderUri = 'https://exolix.com/transaction/' | ||
| const uri = 'https://exolix.com/api/v2/' | ||
|
|
||
| const expirationMs = 1000 * 60 | ||
|
|
||
| const asRateResponse = asObject({ | ||
| minAmount: asNumber, | ||
| withdrawMin: asOptional(asNumber, 0), | ||
| maxAmount: asNumber, | ||
| withdrawMin: asOptional(asNumber), | ||
| withdrawMax: asOptional(asNumber), | ||
| fromAmount: asNumber, | ||
| toAmount: asNumber, | ||
| message: asEither(asString, asNull) | ||
| message: asEither(asString, asNull), | ||
| rateId: asOptional(asString) | ||
| }) | ||
|
|
||
| const asQuoteInfo = asObject({ | ||
|
|
@@ -133,6 +166,18 @@ export function makeExolixPlugin(opts: EdgeCorePluginOptions): EdgeSwapPlugin { | |
| request: EdgeSwapRequestPlugin, | ||
| _userSettings: Object | undefined | ||
| ): Promise<SwapOrder> => { | ||
| const { fromWallet, toWallet, quoteFor } = request | ||
|
|
||
| const networkFrom = getNetwork(fromWallet) | ||
| const networkTo = getNetwork(toWallet) | ||
|
|
||
| if (networkFrom == null || networkTo == null) { | ||
| throw new SwapCurrencyError(swapInfo, request) | ||
| } | ||
|
|
||
| const networkFromChainId = fromWallet.currencyInfo.evmChainId | ||
| const networkToChainId = toWallet.currencyInfo.evmChainId | ||
|
|
||
| async function call( | ||
| method: 'GET' | 'POST', | ||
| route: string, | ||
|
|
@@ -163,13 +208,9 @@ export function makeExolixPlugin(opts: EdgeCorePluginOptions): EdgeSwapPlugin { | |
|
|
||
| if (!response.ok) { | ||
| if (response.status === 422) { | ||
| // Exolix inconsistently returns a !ok response for a 'from' quote | ||
| // under minimum amount, while the status is OK for a 'to' quote under | ||
| // minimum amount. | ||
| // Handle this inconsistency and ensure parse the proper under min error | ||
| // and we don't exit early with the wrong 'unsupported' error message. | ||
| const resJson = await response.json() | ||
| const maybeMinError = asMaybe(asRateResponse)(resJson) | ||
|
|
||
| if (maybeMinError != null) { | ||
| return resJson | ||
| } | ||
|
|
@@ -194,47 +235,48 @@ export function makeExolixPlugin(opts: EdgeCorePluginOptions): EdgeSwapPlugin { | |
| ) | ||
| ]) | ||
|
|
||
| const exchangeQuoteAmount = | ||
| request.quoteFor === 'from' | ||
| ? nativeToDenomination( | ||
| request.fromWallet, | ||
| request.nativeAmount, | ||
| request.fromTokenId | ||
| ) | ||
| : nativeToDenomination( | ||
| request.toWallet, | ||
| request.nativeAmount, | ||
| request.toTokenId | ||
| ) | ||
|
|
||
| const quoteAmount = parseFloat(exchangeQuoteAmount) | ||
|
|
||
| const { | ||
| fromCurrencyCode, | ||
| toCurrencyCode, | ||
| fromMainnetCode, | ||
| toMainnetCode | ||
| } = getCodesWithTranscription(request, MAINNET_CODE_TRANSCRIPTION) | ||
|
|
||
| const quoteParams: Record<string, any> = { | ||
| coinFrom: fromCurrencyCode, | ||
| coinFromNetwork: fromMainnetCode, | ||
| coinTo: toCurrencyCode, | ||
| coinToNetwork: toMainnetCode, | ||
| amount: quoteAmount, | ||
| rateType: 'fixed' | ||
| let amount | ||
| if (quoteFor === 'from') { | ||
| const quoteAmount = nativeToDenomination( | ||
| request.fromWallet, | ||
| request.nativeAmount, | ||
| request.fromTokenId | ||
| ) | ||
| amount = { amount: quoteAmount } | ||
| } else { | ||
| const quoteAmount = nativeToDenomination( | ||
| request.toWallet, | ||
| request.nativeAmount, | ||
| request.toTokenId | ||
| ) | ||
| amount = { withdrawalAmount: quoteAmount } | ||
| } | ||
|
|
||
| // Set the withdrawal amount if we are quoting for the toCurrencyCode | ||
| if (request.quoteFor === 'to') { | ||
| quoteParams.withdrawalAmount = quoteAmount | ||
| const { | ||
| fromContractAddress: coinAddressFrom, | ||
| toContractAddress: coinAddressTo | ||
| } = getContractAddresses(request) | ||
|
|
||
| const quoteParams: ExolixQuoteParams = { | ||
| ...(coinAddressFrom != null ? { coinAddressFrom } : {}), | ||
| ...(networkFromChainId != null ? { networkFromChainId } : {}), | ||
| networkFrom, | ||
| ...(coinAddressTo != null ? { coinAddressTo } : {}), | ||
| ...(networkToChainId != null ? { networkToChainId } : {}), | ||
| networkTo, | ||
| rateType: 'fixed', | ||
| withdrawalAddress: toAddress, | ||
| refundAddress: fromAddress, | ||
| refundExtraId: '', | ||
| withdrawalExtraId: '', | ||
| ...amount | ||
| } | ||
|
|
||
| // Get Rate | ||
| const rateResponse = asRateResponse(await call('GET', 'rate', quoteParams)) | ||
|
|
||
| // Check rate minimum: | ||
| if (request.quoteFor === 'from') { | ||
| if (quoteFor === 'from') { | ||
| const nativeMin = denominationToNative( | ||
| request.fromWallet, | ||
| rateResponse.minAmount.toString(), | ||
|
|
@@ -244,7 +286,21 @@ export function makeExolixPlugin(opts: EdgeCorePluginOptions): EdgeSwapPlugin { | |
| if (lt(request.nativeAmount, nativeMin)) { | ||
| throw new SwapBelowLimitError(swapInfo, nativeMin, 'from') | ||
| } | ||
|
|
||
| const nativeMax = denominationToNative( | ||
| request.fromWallet, | ||
| rateResponse.maxAmount.toString(), | ||
| request.fromTokenId | ||
| ) | ||
|
|
||
| if (gt(request.nativeAmount, nativeMax)) { | ||
| throw new SwapAboveLimitError(swapInfo, nativeMax, 'from') | ||
| } | ||
| } else { | ||
| if (typeof rateResponse.withdrawMin === 'undefined') { | ||
| throw new SwapBelowLimitError(swapInfo, '0', 'to') | ||
| } | ||
|
|
||
| const nativeMin = denominationToNative( | ||
| request.toWallet, | ||
| rateResponse.withdrawMin.toString(), | ||
|
|
@@ -254,26 +310,29 @@ export function makeExolixPlugin(opts: EdgeCorePluginOptions): EdgeSwapPlugin { | |
| if (lt(request.nativeAmount, nativeMin)) { | ||
| throw new SwapBelowLimitError(swapInfo, nativeMin, 'to') | ||
| } | ||
|
|
||
| if (typeof rateResponse.withdrawMax === 'undefined') { | ||
| throw new SwapAboveLimitError(swapInfo, '0', 'to') | ||
| } | ||
|
|
||
| const nativeMax = denominationToNative( | ||
| request.toWallet, | ||
| rateResponse.withdrawMax.toString(), | ||
| request.toTokenId | ||
| ) | ||
|
|
||
| if (gt(request.nativeAmount, nativeMax)) { | ||
| throw new SwapAboveLimitError(swapInfo, nativeMax, 'to') | ||
| } | ||
| } | ||
|
|
||
| // Make the transaction: | ||
| const exchangeParams: Record<string, any> = { | ||
| coinFrom: quoteParams.coinFrom, | ||
| networkFrom: quoteParams.coinFromNetwork, | ||
| coinTo: quoteParams.coinTo, | ||
| networkTo: quoteParams.coinToNetwork, | ||
| amount: quoteAmount, | ||
| withdrawalAddress: toAddress, | ||
| withdrawalExtraId: '', | ||
| refundAddress: fromAddress, | ||
| refundExtraId: '', | ||
| rateType: 'fixed' | ||
| const exchangeParams: ExolixQuoteParams = { | ||
| ...quoteParams, | ||
| rateId: rateResponse.rateId | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Warning: This change removes the prior local Recommendation: Confirm policy intent and keep a local cap if still required.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I confirm this — it was necessary for the old functionality; now all limits are calculated automatically. |
||
| } | ||
|
|
||
| // Set the withdrawal amount if we are quoting for the toCurrencyCode | ||
| if (request.quoteFor === 'to') { | ||
| exchangeParams.withdrawalAmount = quoteAmount | ||
| } | ||
|
|
||
| const callJson = await call('POST', 'transactions', exchangeParams) | ||
| const quoteInfo = asQuoteInfo(callJson) | ||
|
|
@@ -367,71 +426,6 @@ export function makeExolixPlugin(opts: EdgeCorePluginOptions): EdgeSwapPlugin { | |
| const fixedOrder = await getFixedQuote(newRequest, userSettings) | ||
| const fixedResult = await makeSwapPluginQuote(fixedOrder) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Warning: This PR materially changes Exolix quote behavior (new request shape, max/min handling, and 422 parsing) without corresponding automated tests. Recommendation: Add targeted tests for
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From the API requirements angle, this PR does move the integration in the right direction in a few meaningful ways: it updates the request shape to use chain-aware identifiers ( The highest-priority remaining concern is whether the new destination-side quoting path is fully reliable in practice. Because Beyond that, the other material API-requirements items are provider capabilities that matter to user experience and ops, but are not really proven by this plugin diff: an unauthenticated status page, a transaction-status API by |
||
|
|
||
| // Limit exolix to $70k USD | ||
| let currencyCode: string | ||
| let exchangeAmount: string | ||
| let denomToNative: string | ||
| if (newRequest.quoteFor === 'from') { | ||
| currencyCode = newRequest.fromCurrencyCode | ||
| exchangeAmount = nativeToDenomination( | ||
| newRequest.fromWallet, | ||
| newRequest.nativeAmount, | ||
| newRequest.fromTokenId | ||
| ) | ||
| denomToNative = denominationToNative( | ||
| newRequest.fromWallet, | ||
| '1', | ||
| newRequest.fromTokenId | ||
| ) | ||
| } else { | ||
| currencyCode = newRequest.toCurrencyCode | ||
| exchangeAmount = nativeToDenomination( | ||
| newRequest.toWallet, | ||
| newRequest.nativeAmount, | ||
| newRequest.toTokenId | ||
| ) | ||
| denomToNative = denominationToNative( | ||
| newRequest.toWallet, | ||
| '1', | ||
| newRequest.toTokenId | ||
| ) | ||
| } | ||
| const data = [{ currency_pair: `${currencyCode}_iso:USD` }] | ||
|
|
||
| const options = { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ data }) | ||
| } | ||
| let rates: RatesRespose | ||
| try { | ||
| const response = await fetchRates(fetch, 'v2/exchangeRates', options) | ||
| if (!response.ok) { | ||
| const text = await response.text() | ||
| throw new Error(`Error fetching rates: ${text}`) | ||
| } | ||
| const reply = await response.json() | ||
| rates = asRatesResponse(reply) | ||
| } catch (e) { | ||
| log.error('Error fetching rates', String(e)) | ||
| throw new Error('Error fetching rates') | ||
| } | ||
|
|
||
| const { exchangeRate } = rates.data[0] | ||
| if (exchangeRate == null) throw new SwapCurrencyError(swapInfo, request) | ||
|
|
||
| const usdValue = mul(exchangeAmount, exchangeRate) | ||
| const maxExchangeAmount = div18(MAX_USD_VALUE, exchangeRate) | ||
| const maxNativeAmount = mul(maxExchangeAmount, denomToNative) | ||
|
|
||
| if (gt(usdValue, MAX_USD_VALUE)) { | ||
| throw new SwapAboveLimitError( | ||
| swapInfo, | ||
| maxNativeAmount, | ||
| newRequest.quoteFor === 'from' ? 'from' : 'to' | ||
| ) | ||
| } | ||
|
|
||
| return fixedResult | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Issue: The 422 fallback uses
asMaybe(asRateResponse), but this cleaner now requires fields likemaxAmountandrateId. If Exolix returns a partial 422/minimum payload, parsing fails and this path throwsSwapCurrencyErrorinstead of returning a precise limit error.Recommendation: Use a dedicated looser cleaner for 422 limit payloads, and reserve
asRateResponsefor successful quote payloads.