Feat: Changelly integration#449
Conversation
Signed-off-by: Changelly <changelly@users.noreply.github.com>
Signed-off-by: Changelly <changelly@users.noreply.github.com>
| toCurrencyCode | ||
| } = getCodesWithTranscription(request, MAINNET_CODE_TRANSCRIPTION) | ||
|
|
||
| const fromTicker = |
There was a problem hiding this comment.
getCodesWithTranscription() is the wrong abstraction for this integration. That helper is useful for providers that model assets as separate chainCode + tokenCode values and mostly key off currency codes. Changelly does not work that way here. It exposes a single provider asset id per supported asset, and this PR only passes the mainnet transcription map, so the returned Changelly IDs would land in fromMainnetCode / toMainnetCode anyway. The quote path never uses those values, so the mapped Changelly IDs are effectively ignored.
There is a second problem in the same area: falling back to fromCurrencyCode.toLowerCase() / toCurrencyCode.toLowerCase() when the asset is not found in the provider cache is unsafe. If the cache lookup misses, the plugin can silently request a quote for a different asset than the one the user selected. ETH is the obvious example, since many distinct chains use that currency code.
A better approach would be to build the cache as a mapping from Changelly-supported assets to local pluginId / tokenId pairs using the provider payload, especially contractAddress. Convert the Changelly supported assets response to a ChainCodeTickerMap and then use that with the getChainAndTokenCodes utility. Using the returned fromCurrencyCode and toCurrencyCode from that function would result in the correct Changelly IDs for the quote. Changehero, Changenow, Letsexchange, and Sideshift have all adopted this pattern.
| if (profiles.length === 0) throw new Error('Unable to detect user params') | ||
|
|
||
| return { | ||
| userId: profiles[0].userId, |
There was a problem hiding this comment.
Let's use initOptions (passed in via EdgeCorePluginOptions) instead of using disklet. We can pass in any arbitrary fields necessary.
There was a problem hiding this comment.
These options are integral to our new 'relay' strategy. We have opted to bypass endpoint authentication via provided credentials in favor of authenticating edge users through non-sensitive data stored locally(zero-config authentication).
There was a problem hiding this comment.
The data accessed in this blob are unique to the user and don't identify them as Edge users. Also, it passes the username of any account that attempts a swap to Changelly, which is non-starter. We don't associate usernames with swaps and won't pass them along. Instead, ignore the local disklet completely and accept the same string the Changelly reports-server PR is expecting "edge-app".
There was a problem hiding this comment.
We've rolled out usage of a disklet from our client. Now it's pseudo-sequence that is related to a one, used in reports server.
Signed-off-by: Changelly <changelly@users.noreply.github.com>
Signed-off-by: Changelly <changelly@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 5 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d3d058d. Configure here.
|
|
||
| const headers: Record<string, string> = { | ||
| 'Content-Type': 'application/json', | ||
| 'X-Auth': btoa([params.a, params.b, params.c].join(':')) |
There was a problem hiding this comment.
Obfuscated hardcoded API credentials in source code
High Severity
API credentials are hardcoded in the source code using XOR obfuscation (params.a decodes to an authentication key via character code XOR with a constant derived from Math.PI * Math.E * 4.913456). Every other central swap plugin (changehero, changenow, exolix, godex) retrieves API keys from initOptions passed via EdgeCorePluginOptions. Embedding credentials directly in source — even obfuscated — exposes them in the repository and was explicitly rejected in PR discussion.
Reviewed by Cursor Bugbot for commit d3d058d. Configure here.
| const jsonBody = JSON.stringify({ | ||
| ...body, | ||
| jsonrpc: '2.0', | ||
| id: body.method + ':' + String(params.b) |
There was a problem hiding this comment.
Obfuscated identifier computes "test" instead of "edge-app"
High Severity
params.b is computed via arithmetic obfuscation ((18 >> 1) * 11 + 17 = 116, then [0, -15, -1, 0].map(x => String.fromCharCode(116 + x))) producing the string "test". This value is sent as part of the X-Auth header and JSON-RPC id field. The PR discussion explicitly states this identifier must be "edge-app", not a test placeholder.
Reviewed by Cursor Bugbot for commit d3d058d. Configure here.
| const minNativeAmount = denominationToNative( | ||
| wallet, | ||
| minFrom, | ||
| tokenId |
There was a problem hiding this comment.
Reverse quote limit uses wrong wallet for denomination
Medium Severity
When reverseQuote is true, the code selects request.toWallet/request.toTokenId for denomination conversion, but reads limits.min?.from / limits.max?.from which are always denominated in the "from" currency. This applies the wrong token multiplier, producing incorrect native amounts for SwapBelowLimitError and SwapAboveLimitError.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit d3d058d. Configure here.
| const minNativeAmount = denominationToNative( | ||
| wallet, | ||
| minFrom, | ||
| tokenId |
There was a problem hiding this comment.
Limit native amounts not rounded to integers
Medium Severity
The denominationToNative results for minNativeAmount and maxNativeAmount are not rounded to integer atomic units before being passed to SwapBelowLimitError and SwapAboveLimitError. Exchange providers may return amounts with excess decimal precision, producing fractional native amounts. The main quote amounts at lines 510-520 are correctly truncated via .split('.')[0], but the limit amounts are not.
Additional Locations (1)
Triggered by learned rule: Round denominationToNative results to integer atomic units in swap plugins
Reviewed by Cursor Bugbot for commit d3d058d. Configure here.
|
|
||
| chainCodeTickerMap = out | ||
| lastUpdated = Date.now() | ||
| } catch (e) { |
There was a problem hiding this comment.
Catch clause missing unknown type annotation
Low Severity
The catch (e) block in fetchSupportedAssets does not type the error parameter as unknown. Per project TypeScript convention, all catch clauses must use catch (e: unknown) for type-safe error handling.
Triggered by learned rule: Use catch (e: unknown) instead of catch (e) per project TypeScript standards
Reviewed by Cursor Bugbot for commit d3d058d. Configure here.


CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
PR's contains no dependencies.
Description
PR introduces new Changelly Relay API matching the actual JSON-RPC envelope.
Note
Medium Risk
Adds a new centralized swap integration that generates spend transactions and makes authenticated JSON-RPC calls to Changelly; correctness depends on API behavior, mapping accuracy, and error translation (limits/permission).
Overview
Adds Changelly as a new centralized swap plugin (
src/swap/central/changelly.ts) that fetches supported assets, requests fixed-rate quotes, creates swap transactions, and translates API errors intoSwapBelowLimitError/SwapAboveLimitError/SwapPermissionError.Introduces Changelly currency/network transcription mappings (
scripts/mappings/changellyMappings.ts→ auto-generatedsrc/mappings/changelly.ts) and registers the plugin insrc/index.ts. UpdatesCHANGELOG.mdwith an unreleased entry for Changelly support.Reviewed by Cursor Bugbot for commit d3d058d. Bugbot is set up for automated code reviews on this repo. Configure here.