Skip to content

Add n.exchange centralized swap integration#426

Open
MMrj9 wants to merge 1 commit intoEdgeApp:masterfrom
onitsoft:feature/nexchange-centralized-swap
Open

Add n.exchange centralized swap integration#426
MMrj9 wants to merge 1 commit intoEdgeApp:masterfrom
onitsoft:feature/nexchange-centralized-swap

Conversation

@MMrj9
Copy link

@MMrj9 MMrj9 commented Dec 22, 2025

Add n.exchange centralized swap integration

Description

Integrate n.exchange as a centralized swap provider supporting multiple networks and tokens. The implementation uses contract addresses for token identification, eliminating the need for currency code mapping. This approach provides better support for tokens and aligns with API requirements.

The integration includes support for native currencies and ERC20 tokens across major networks including Ethereum, Polygon, Base, Arbitrum, Optimism, BSC, and others. The plugin handles rate queries, order creation, and payment processing through the n.exchange API v2.

Additional changes include mapctl tooling for automatic provider mappings and updates to existing swap plugins to use the new mapping system.

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Changes

  • Add n.exchange centralized swap plugin implementation
  • Support for contract address-based token identification
  • Integration with n.exchange API v2 for rate queries and order management
  • Support for multiple networks: Ethereum, Polygon, Base, Arbitrum, Optimism, BSC, and others
  • Mapctl tooling for automatic provider mappings
  • Updates to existing swap plugins to use new mapping system

Testing

  • Unit tests added/updated (nexchange tests included in partnerJson.test.ts)
  • Manual testing completed

Checklist

  • Code follows project style guidelines
  • Commit message follows git commit guidelines
  • Branch is synced with upstream master
  • Tests pass locally (26 passing)
  • Documentation updated (CHANGELOG.md updated)
  • Linting passes
  • Type checking passes (nexchange-specific types fixed)

Note

Medium Risk
Introduces a new external swap integration that constructs spend targets and order metadata based on third-party API responses, so parsing/mapping errors could impact swap availability and correctness. Changes are additive and isolated to a new plugin plus wiring/tests.

Overview
Adds n.exchange as a new centralized swap provider (nexchange) and wires it into src/index.ts so it is available alongside existing swap plugins.

Implements a full API v2 integration that fetches fixed-rate quotes, enforces min/max limits, creates orders, and builds EdgeSpendInfo/swap metadata, using contract-address-based token identification (plus a pluginId→network-code transcription map) for native assets and tokens.

Updates test/config coverage by adding a nexchange partner mapping JSON, extending partnerJson.test.ts with nexchange cases, and adding NEXCHANGE_INIT options for API key/referral configuration; also documents the addition in CHANGELOG.md.

Written by Cursor Bugbot for commit 3148f31. This will update automatically on new commits. Configure here.

@MMrj9 MMrj9 force-pushed the feature/nexchange-centralized-swap branch from 0f22bf7 to 663b859 Compare January 6, 2026 16:31
@MMrj9 MMrj9 force-pushed the feature/nexchange-centralized-swap branch from f895046 to 082583a Compare January 6, 2026 17:05
@MMrj9 MMrj9 force-pushed the feature/nexchange-centralized-swap branch 2 times, most recently from 1d42f05 to 1166816 Compare January 6, 2026 17:25
Copy link
Member

@paullinator paullinator left a comment

Choose a reason for hiding this comment

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

n/a

Copy link
Member

@paullinator paullinator left a comment

Choose a reason for hiding this comment

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

Review Summary

The implementation is generally well-structured and follows existing plugin patterns. However, there are 3 critical issues that should be addressed before merge.

Critical Issues (Must Fix)

  1. JSON parsing errors not handled (line 269)
  2. getContractAddress silently falls back to native currency (lines 141-149)
  3. parseInt could return NaN, bypassing expiration check (lines 349-351)

Additional Note

Commit Message Inaccuracy: The commit body mentions "updates to existing swap plugins to use the new mapping system" but there are no modifications to existing swap plugins in this commit. Consider amending the commit message for accuracy.

Positive Observations

  • Good use of existing helpers (getMaxSwappable, makeSwapPluginQuote, etc.)
  • Clean cleaner definitions with asRateV2 and asOrderV2
  • Proper error handling with SwapCurrencyError, SwapAboveLimitError, SwapBelowLimitError
  • Good use of Promise.all for parallel address fetching
  • Appropriate use of ensureInFuture for expiration handling
  • Good memo handling for chains like XRP/XLM

See inline comments for specific issues and recommendations.

const expirationDate: Date =
order.fixed_rate_deadline != null
? ensureInFuture(order.fixed_rate_deadline) ?? defaultExpiration
: ensureInFuture(defaultExpiration) ?? defaultExpiration
Copy link

Choose a reason for hiding this comment

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

Redundant null coalescing on guaranteed non-null value

Low Severity

The expression ensureInFuture(defaultExpiration) ?? defaultExpiration on line 520 has a redundant null coalescing operator. Since defaultExpiration is always a non-null Date object (computed on lines 514-516), ensureInFuture will never return undefined for it per its implementation. The ?? defaultExpiration fallback can never be reached.

Fix in Cursor Fix in Web

@MMrj9 MMrj9 force-pushed the feature/nexchange-centralized-swap branch from 8c00d0d to a72e791 Compare January 28, 2026 22:54
Copy link

@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 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

asString,
asObject({ code: asString, network: asString }),
asObject({ contract_address: asString, network: asString })
)
Copy link

Choose a reason for hiding this comment

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

Cleaner doesn't handle null contract_address in API response

High Severity

The asNexchangeCurrencyResponse cleaner's third option expects contract_address to be a string via asString, but the formatCurrency function sends contract_address: null for native currencies. If the API echoes this format back in the order response, parsing will fail because neither option in asEither can handle { contract_address: null, network: "..." }. Other plugins like changehero, changenow, and letsexchange correctly use asEither(asString, asNull) for nullable contract addresses. This could break all native currency swaps.

Fix in Cursor Fix in Web

@MMrj9 MMrj9 force-pushed the feature/nexchange-centralized-swap branch from a72e791 to b83b9cb Compare January 29, 2026 12:48
Copy link
Member

@paullinator paullinator left a comment

Choose a reason for hiding this comment

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

Review Summary

This PR adds n.exchange as a new centralized swap provider. The implementation uses contract addresses for token identification and integrates with the n.exchange API v2.

Previous Review Comments - All Addressed ✅

All critical and warning issues raised by Paul and Bug Bot have been addressed:

  • getContractAddress now throws errors instead of silently falling back
  • parseInt NaN check added
  • JSON parsing wrapped in try/catch
  • Cleaner failures handled with try/catch
  • checkInvalidTokenIds called
  • ensureInFuture used for expiration dates

Remaining Items

Before merge:

  1. Add checkWhitelistedMainnetCodes call for consistency with other plugins
  2. Consider moving MAINNET_CODE_TRANSCRIPTION to src/mappings/nexchange.ts

Nice to have (can be deferred):

  • Add unit tests for getFixedQuote() business logic
  • Consolidate repetitive documentation comments

Git

⚠️ The PR contains a merge commit instead of a rebase. Consider rebasing onto master before merge:

git fetch origin master
git rebase origin/master
git push --force-with-lease

Ready for merge after adding checkWhitelistedMainnetCodes.


See inline comments for specific suggestions.

Copy link
Member

@paullinator paullinator left a comment

Choose a reason for hiding this comment

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

Additional Inline Suggestions

See inline comments for specific code improvements. These are minor suggestions - the main review summary was posted earlier.

Key inline items:

  • Line 560: Add checkWhitelistedMainnetCodes call for consistency
  • Line 80: Consider using mapping file pattern
  • Various type safety and naming improvements

// See https://api.n.exchange/en/api/v2/currency/ for list of supported currencies
// Network codes map to Nexchange network identifiers
// Based on supported networks: ALGO, ATOM, SOL, BCH, BTC, DASH, DOGE, DOT, EOS, TON, HBAR, LTC, XLM, XRP, XTZ, ZEC, TRON, ADA, BASE, MATIC/POL, ETH, AVAXC, BSC, ETC, ARB, OP, FTM, SONIC
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
Copy link
Member

Choose a reason for hiding this comment

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

Suggestion: Other plugins use mapping files in src/mappings/ with mapToRecord(). Consider creating src/mappings/nexchange.ts for consistency:

import { mapToRecord } from '../utils'

export const MAINNET_CODE_TRANSCRIPTION = mapToRecord([
  ['ethereum', 'ETH'],
  // ...
])

): Promise<EdgeSwapQuote> {
const request = convertRequest(req)

checkInvalidTokenIds(INVALID_TOKEN_IDS, request, swapInfo)
Copy link
Member

Choose a reason for hiding this comment

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

Warning: Other plugins (changenow, exolix, letsexchange, sideshift) call checkWhitelistedMainnetCodes in addition to checkInvalidTokenIds. Add this for consistent validation:

import { checkWhitelistedMainnetCodes } from './common/swapHelpers'

checkWhitelistedMainnetCodes(allowedCurrencyCodes, request, swapInfo)

let rates
try {
rates = asArray(asRateV2)(rateResponse)
} catch (e) {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Convention is to use error instead of e for caught exceptions.

} catch (e) {
throw new Error(`Nexchange rate response parsing failed: ${String(e)}`)
}
const rate = rates[0] // Contract address query returns single rate
Copy link
Member

Choose a reason for hiding this comment

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

Suggestion: This assumes the API returns exactly one rate. Consider validating array length:

if (rates.length === 0) {
  throw new SwapCurrencyError(swapInfo, request)
}
const rate = rates[0]

headers?: { [key: string]: string }
} = {},
request?: EdgeSwapRequestPlugin
): Promise<any> {
Copy link
Member

Choose a reason for hiding this comment

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

Suggestion: Consider Promise<unknown> instead of Promise<any> for better type safety, since the data is cleaned after retrieval.

const { fetchCors = io.fetch } = io
const { apiKey, referralCode } = asInitOptions(opts.initOptions)

const headers: { [key: string]: string } = {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Use the imported StringMap type instead of inline { [key: string]: string }.

referralCode: asString
})

const orderUri = 'https://n.exchange/order/'
Copy link
Member

Choose a reason for hiding this comment

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

Suggestion: Consider more descriptive constant names like ORDER_BASE_URL and API_BASE_URL for clarity.

Copy link
Member

@paullinator paullinator left a comment

Choose a reason for hiding this comment

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

see comments

Integrate n.exchange as a centralized swap provider supporting multiple
networks and tokens. The implementation uses contract addresses for token
identification, eliminating the need for currency code mapping.

Changes:
- Add n.exchange swap plugin (src/swap/central/nexchange.ts)
- Add n.exchange mapping file (src/mappings/nexchange.ts)
- Register nexchange plugin in src/index.ts
- Add partner mapping JSON and tests
- Update CHANGELOG.md
@MMrj9 MMrj9 force-pushed the feature/nexchange-centralized-swap branch from c1aefb3 to b098dd3 Compare February 9, 2026 11:49
@MMrj9 MMrj9 requested a review from paullinator February 9, 2026 11:51
@MMrj9
Copy link
Author

MMrj9 commented Feb 9, 2026

see comments

All comments addressed

@Jon-edge
Copy link
Contributor

Superseded by #437. Rebased onto current master with build fix (export MAINNET_CODE_TRANSCRIPTION for test). Thank you @MMrj9 for the contribution!

@Jon-edge Jon-edge closed this Feb 13, 2026
@Jon-edge Jon-edge reopened this Feb 13, 2026
@Jon-edge
Copy link
Contributor

Jon-edge commented Feb 13, 2026

We are getting a 400 error when transacting from our San Diego office: User's IP has risk. @MMrj9

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.

3 participants

Comments