Skip to content

Feat: Changelly integration#449

Open
changelly wants to merge 4 commits into
EdgeApp:masterfrom
changelly:feat_changelly_integration
Open

Feat: Changelly integration#449
changelly wants to merge 4 commits into
EdgeApp:masterfrom
changelly:feat_changelly_integration

Conversation

@changelly
Copy link
Copy Markdown

@changelly changelly commented Mar 27, 2026

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

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 into SwapBelowLimitError/SwapAboveLimitError/SwapPermissionError.

Introduces Changelly currency/network transcription mappings (scripts/mappings/changellyMappings.ts → auto-generated src/mappings/changelly.ts) and registers the plugin in src/index.ts. Updates CHANGELOG.md with 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.

Signed-off-by: Changelly <changelly@users.noreply.github.com>
Signed-off-by: Changelly <changelly@users.noreply.github.com>
Comment thread src/swap/central/changelly.ts Outdated
toCurrencyCode
} = getCodesWithTranscription(request, MAINNET_CODE_TRANSCRIPTION)

const fromTicker =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Requested changes were provided with b3c7bd5

Comment thread src/swap/central/changelly.ts Outdated
if (profiles.length === 0) throw new Error('Unable to detect user params')

return {
userId: profiles[0].userId,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's use initOptions (passed in via EdgeCorePluginOptions) instead of using disklet. We can pass in any arbitrary fields necessary.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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".

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 5 potential issues.

Fix All in Cursor

❌ 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(':'))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d3d058d. Configure here.

const jsonBody = JSON.stringify({
...body,
jsonrpc: '2.0',
id: body.method + ':' + String(params.b)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d3d058d. Configure here.

const minNativeAmount = denominationToNative(
wallet,
minFrom,
tokenId
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d3d058d. Configure here.

const minNativeAmount = denominationToNative(
wallet,
minFrom,
tokenId
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants