Skip to content
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- added: n.exchange DeFi swap plugin (`nexchangedefi`) — API v2 with `is_defi` on `/rate/` and `is_defi: true` on order creation (NC-Bridge flow; same chain mapping as CeFi n.exchange)

## 2.43.0 (2026-03-10)

- added: Xgram support
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { makeBridgelessPlugin } from './swap/defi/bridgeless'
import { makeCosmosIbcPlugin } from './swap/defi/cosmosIbc'
import { makeFantomSonicUpgradePlugin } from './swap/defi/fantomSonicUpgrade'
import { makeLifiPlugin } from './swap/defi/lifi'
import { makeNexchangeDefiPlugin } from './swap/defi/nexchangeDefi'
import { makeRangoPlugin } from './swap/defi/rango'
import { makeMayaProtocolPlugin } from './swap/defi/thorchain/mayaprotocol'
import { makeSwapKitPlugin } from './swap/defi/thorchain/swapkit'
Expand All @@ -39,6 +40,7 @@ const plugins = {
letsexchange: makeLetsExchangePlugin,
lifi: makeLifiPlugin,
nexchange: makeNexchangePlugin,
nexchangedefi: makeNexchangeDefiPlugin,
rango: makeRangoPlugin,
sideshift: makeSideshiftPlugin,
spookySwap: makeSpookySwapPlugin,
Expand Down
70 changes: 55 additions & 15 deletions src/swap/central/nexchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,32 @@ import {
} from '../../util/utils'
import { EdgeSwapRequestPlugin, StringMap } from '../types'

const pluginId = 'nexchange'
const pluginIdCefi = 'nexchange'
const pluginIdDefi = 'nexchangedefi'

const NEXCHANGE_MODE_CEFI = 'cefi' as const
const NEXCHANGE_MODE_DEFI = 'defi' as const

type NexchangePluginMode =
| typeof NEXCHANGE_MODE_CEFI
| typeof NEXCHANGE_MODE_DEFI

/** CeFi (custodial) n.exchange swap plugin metadata */
export const swapInfo: EdgeSwapInfo = {
pluginId,
pluginId: pluginIdCefi,
isDex: false,
displayName: 'n.exchange',
supportEmail: 'support@n.exchange'
}

/** DeFi (NC-Bridge) n.exchange swap plugin metadata — same API v2 with `is_defi` on rate and order */
export const swapInfoDefi: EdgeSwapInfo = {
pluginId: pluginIdDefi,
isDex: true,
displayName: 'n.exchange DeFi',
supportEmail: 'support@n.exchange'
}

const asInitOptions = asObject({
apiKey: asString,
referralCode: asOptional(asString, '')
Expand Down Expand Up @@ -166,6 +183,22 @@ const asOrderV2 = asObject({
export function makeNexchangePlugin(
opts: EdgeCorePluginOptions
): EdgeSwapPlugin {
return makeNexchangePluginInner(opts, NEXCHANGE_MODE_CEFI)
}

/** DeFi routing via NC-Bridge: [API v2 rate](https://api.n.exchange/docs/v2/) with `is_defi=true` and orders with `is_defi: true`. */
export function makeNexchangeDefiPlugin(
opts: EdgeCorePluginOptions
): EdgeSwapPlugin {
return makeNexchangePluginInner(opts, NEXCHANGE_MODE_DEFI)
}

function makeNexchangePluginInner(
opts: EdgeCorePluginOptions,
mode: NexchangePluginMode
): EdgeSwapPlugin {
const activeSwapInfo: EdgeSwapInfo =
mode === NEXCHANGE_MODE_DEFI ? swapInfoDefi : swapInfo
const { io, log } = opts
const { apiKey, referralCode } = asInitOptions(opts.initOptions)

Expand Down Expand Up @@ -198,15 +231,15 @@ export function makeNexchangePlugin(
log.warn('Nexchange response:', text)

if (response.status === 404 && request != null) {
throw new SwapCurrencyError(swapInfo, request)
throw new SwapCurrencyError(activeSwapInfo, request)
}

if (response.status === 400) {
const errorData = asErrorResponse(text)
if (
errorData?.non_field_errors.includes("User's IP has risk.") === true
) {
throw new SwapPermissionError(swapInfo, 'geoRestriction')
throw new SwapPermissionError(activeSwapInfo, 'geoRestriction')
}
}

Expand Down Expand Up @@ -250,7 +283,7 @@ export function makeNexchangePlugin(
]

if (fromMainnetCode == null || toMainnetCode == null) {
throw new SwapCurrencyError(swapInfo, request)
throw new SwapCurrencyError(activeSwapInfo, request)
}

const {
Expand Down Expand Up @@ -279,6 +312,9 @@ export function makeNexchangePlugin(
params.append('from_network', fromMainnetCode)
params.append('to_contract_address', toContractAddress ?? '')
params.append('to_network', toMainnetCode)
if (mode === NEXCHANGE_MODE_DEFI) {
params.append('is_defi', 'true')
}

const rateResponse = await fetchNexchange(
`/rate/?${params.toString()}`,
Expand All @@ -295,7 +331,7 @@ export function makeNexchangePlugin(
}

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

Expand All @@ -319,10 +355,10 @@ export function makeNexchangePlugin(
)

if (gt(quoteAmount, rate.max_deposit_amount)) {
throw new SwapAboveLimitError(swapInfo, maxFromNative)
throw new SwapAboveLimitError(activeSwapInfo, maxFromNative)
}
if (lt(quoteAmount, rate.min_deposit_amount)) {
throw new SwapBelowLimitError(swapInfo, minFromNative)
throw new SwapBelowLimitError(activeSwapInfo, minFromNative)
}
} else {
// We're quoting based on withdraw amount (what we receive)
Expand All @@ -338,10 +374,10 @@ export function makeNexchangePlugin(
)

if (gt(quoteAmount, rate.max_withdraw_amount)) {
throw new SwapAboveLimitError(swapInfo, maxToNative, 'to')
throw new SwapAboveLimitError(activeSwapInfo, maxToNative, 'to')
}
if (lt(quoteAmount, rate.min_withdraw_amount)) {
throw new SwapBelowLimitError(swapInfo, minToNative, 'to')
throw new SwapBelowLimitError(activeSwapInfo, minToNative, 'to')
}
}

Expand All @@ -366,6 +402,7 @@ export function makeNexchangePlugin(
withdraw_address: string
refund_address: string
rate_id: string
is_defi?: boolean
deposit_amount?: string
withdraw_amount?: string
} = {
Expand All @@ -375,6 +412,9 @@ export function makeNexchangePlugin(
refund_address: fromAddress,
rate_id: rate.rate_id
}
if (mode === NEXCHANGE_MODE_DEFI) {
orderBody.is_defi = true
}

// Set amount based on quote direction
if (request.quoteFor === 'from') {
Expand Down Expand Up @@ -449,7 +489,7 @@ export function makeNexchangePlugin(
},
savedAction: {
actionType: 'swap',
swapInfo,
swapInfo: activeSwapInfo,
orderUri: ORDER_BASE_URL + order.unique_reference,
orderId: order.unique_reference,
isEstimate: false,
Expand All @@ -472,26 +512,26 @@ export function makeNexchangePlugin(
return {
request,
spendInfo,
swapInfo,
swapInfo: activeSwapInfo,
fromNativeAmount: amountExpectedFromNative,
expirationDate
}
}

const out: EdgeSwapPlugin = {
swapInfo,
swapInfo: activeSwapInfo,
async fetchSwapQuote(
req: EdgeSwapRequest,
userSettings: Object | undefined,
opts: { promoCode?: string } // Reserved for future use
): Promise<EdgeSwapQuote> {
const request = convertRequest(req)

checkInvalidTokenIds(INVALID_TOKEN_IDS, request, swapInfo)
checkInvalidTokenIds(INVALID_TOKEN_IDS, request, activeSwapInfo)
checkWhitelistedMainnetCodes(
MAINNET_CODE_TRANSCRIPTION,
request,
swapInfo
activeSwapInfo
)

const newRequest = await getMaxSwappable(
Expand Down
5 changes: 5 additions & 0 deletions src/swap/defi/nexchangeDefi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export {
MAINNET_CODE_TRANSCRIPTION,
makeNexchangeDefiPlugin,
swapInfoDefi as swapInfo
} from '../central/nexchange'
43 changes: 43 additions & 0 deletions test/partnerJson/partnerJson.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ import {
MAINNET_CODE_TRANSCRIPTION as sideshiftMainnetTranscription,
swapInfo as sideshiftSwapInfo
} from '../../src/swap/central/sideshift'
import {
MAINNET_CODE_TRANSCRIPTION as nexchangeDefiMainnetTranscription,
swapInfo as nexchangeDefiSwapInfo
} from '../../src/swap/defi/nexchangeDefi'
import {
ChainCodeTickerMap,
getChainAndTokenCodes
Expand Down Expand Up @@ -154,6 +158,18 @@ const nexchange = async (request: EdgeSwapRequest): Promise<Codes> => {
nexchangeMainnetTranscription
)
}
const nexchangedefi = async (request: EdgeSwapRequest): Promise<Codes> => {
const nexchangeChainCodeTickerMap = getChainCodeTickerMap(
nexchangeChainCodeTickerJson
)

return await getChainAndTokenCodes(
request,
nexchangeDefiSwapInfo,
nexchangeChainCodeTickerMap,
nexchangeDefiMainnetTranscription
)
}
const sideshift = async (request: EdgeSwapRequest): Promise<Codes> => {
const sideshiftChainCodeTickerMap = getChainCodeTickerMap(
sideshiftChainCodeTickerJson
Expand Down Expand Up @@ -215,6 +231,15 @@ describe(`swap btc to eth`, function () {
toCurrencyCode: 'ETH'
})
})
it('nexchangedefi', async function () {
const result = await nexchangedefi(request)
return assert.deepEqual(result, {
fromMainnetCode: 'BTC',
fromCurrencyCode: 'BTC',
toMainnetCode: 'ETH',
toCurrencyCode: 'ETH'
})
})
it('sideshift', async function () {
const result = await sideshift(request)
return assert.deepEqual(result, {
Expand Down Expand Up @@ -274,6 +299,15 @@ describe(`swap btc to avax`, function () {
toCurrencyCode: 'AVAX'
})
})
it('nexchangedefi', async function () {
const result = await nexchangedefi(request)
return assert.deepEqual(result, {
fromMainnetCode: 'BTC',
fromCurrencyCode: 'BTC',
toMainnetCode: 'AVAXC',
toCurrencyCode: 'AVAX'
})
})
it('sideshift', async function () {
const result = await sideshift(request)
return assert.deepEqual(result, {
Expand Down Expand Up @@ -333,6 +367,15 @@ describe(`swap btc to usdt (avax c-chain)`, function () {
toCurrencyCode: 'USDT'
})
})
it('nexchangedefi', async function () {
const result = await nexchangedefi(request)
return assert.deepEqual(result, {
fromMainnetCode: 'BTC',
fromCurrencyCode: 'BTC',
toMainnetCode: 'AVAXC',
toCurrencyCode: 'USDT'
})
})
it('sideshift', async function () {
const result = await sideshift(request)
return assert.deepEqual(result, {
Expand Down
Loading