From 0ea9afad4b80fa5e6ffd0e6b36e0060f238f271a Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 26 Mar 2026 13:51:00 -0700 Subject: [PATCH 1/4] Add bitcoin cash + small tweaks Add bitcoin cash chain id Add zero address constant Use correct fetch Reaplce switch/case with if for Zano special case --- src/swap/defi/bridgeless.ts | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/swap/defi/bridgeless.ts b/src/swap/defi/bridgeless.ts index 06d6c6b1..76e617d4 100644 --- a/src/swap/defi/bridgeless.ts +++ b/src/swap/defi/bridgeless.ts @@ -48,9 +48,11 @@ const swapInfo: EdgeSwapInfo = { const BASE_URL = 'https://rpc-api.node0.mainnet.bridgeless.com' const ORDER_URL = 'https://tss1.mainnet.bridgeless.com' const AUTO_BOT_URL = 'https://autobot-wusa1.edge.app' +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' const EDGE_PLUGINID_CHAINID_MAP: Record = { bitcoin: '0', + bitcoincash: '5', zano: '2' } @@ -137,7 +139,7 @@ export function makeBridgelessPlugin( wallet: EdgeCurrencyWallet, contractAddress: string ): Promise => { - if (contractAddress === '0x0000000000000000000000000000000000000000') { + if (contractAddress === ZERO_ADDRESS) { return null } else { const fakeToken: EdgeToken = { @@ -158,7 +160,7 @@ export function makeBridgelessPlugin( let toTokenInfo: TokenInfo | undefined while (true) { const pageKeyStr = pageKey == null ? '' : `?pagination.key=${pageKey}` - const raw = await fetchBridgeless(fetch, `/tokens${pageKeyStr}`) + const raw = await fetchBridgeless(opts.io.fetch, `/tokens${pageKeyStr}`) const response = asBridgeTokens(raw) // Find a token object where both from and to infos are present @@ -247,20 +249,10 @@ export function makeBridgelessPlugin( } } - let receiver: string | undefined - switch (request.toWallet.currencyInfo.pluginId) { - case 'bitcoin': { - receiver = toAddress - break - } - case 'zano': { - receiver = base16.encode(base58.decode(toAddress)) - break - } - default: { - throw new SwapCurrencyError(swapInfo, request) - } - } + const receiver = + request.toWallet.currencyInfo.pluginId === 'zano' + ? base16.encode(base58.decode(toAddress)) + : toAddress // chainId/txid/outputIndex // output index is 0 for both Bitcoin (output of actual deposit) and Zano (index of serviceEntries with deposit instructions) @@ -291,7 +283,8 @@ export function makeBridgelessPlugin( } switch (request.fromWallet.currencyInfo.pluginId) { - case 'bitcoin': { + case 'bitcoin': + case 'bitcoincash': { const opReturn = `${receiver}${Buffer.from( `#${toChainId}`, 'utf8' From aabf83ca1792a1b1da63e26e58849c99a8644500 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 26 Mar 2026 16:13:30 -0700 Subject: [PATCH 2/4] Fix asset handling The plugin isn't handling tokenId or decimals correctly. It just kind of worked for bitcoin because the number of decimals matched on both sides of the swap. For tokenId, only the fromWallet was used so the plugin is broken if the swap goes from token->token. --- src/swap/defi/bridgeless.ts | 107 ++++++++++++++++++++++++++++-------- test/utils.test.ts | 34 ++++++++++++ 2 files changed, 117 insertions(+), 24 deletions(-) diff --git a/src/swap/defi/bridgeless.ts b/src/swap/defi/bridgeless.ts index 76e617d4..47fbcfec 100644 --- a/src/swap/defi/bridgeless.ts +++ b/src/swap/defi/bridgeless.ts @@ -113,6 +113,34 @@ const fetchBridgeless = async ( return json } +export const scaleNativeAmount = ( + amount: string, + fromDecimals: number, + toDecimals: number, + round: 'down' | 'up' +): string => { + const diff = toDecimals - fromDecimals + if (diff === 0) return amount + + if (diff > 0) { + return amount + '0'.repeat(diff) + } + + const places = -diff + if (amount.length <= places) { + return round === 'up' && /[1-9]/.test(amount) ? '1' : '0' + } + + const whole = amount.slice(0, -places) + const remainder = amount.slice(-places) + + if (round === 'up' && /[1-9]/.test(remainder)) { + return add(whole, '1') + } + + return whole +} + export function makeBridgelessPlugin( opts: EdgeCorePluginOptions ): EdgeSwapPlugin { @@ -169,26 +197,35 @@ export function makeBridgelessPlugin( let toTokenInfoForToken: TokenInfo | undefined for (const info of token.info) { try { - const tokenId = await getTokenId(request.fromWallet, info.address) - if ( - info.chain_id === - EDGE_PLUGINID_CHAINID_MAP[ - request.fromWallet.currencyInfo.pluginId - ] && - tokenId === request.fromTokenId - ) { - fromTokenInfoForToken = info + if (fromTokenInfoForToken == null) { + const tokenId = await getTokenId(request.fromWallet, info.address) + if ( + info.chain_id === + EDGE_PLUGINID_CHAINID_MAP[ + request.fromWallet.currencyInfo.pluginId + ] && + tokenId === request.fromTokenId + ) { + fromTokenInfoForToken = info + } } - if ( - info.chain_id === - EDGE_PLUGINID_CHAINID_MAP[ - request.toWallet.currencyInfo.pluginId - ] && - tokenId === request.toTokenId - ) { - toTokenInfoForToken = info + } catch (e: unknown) { + // ignore tokens that fail validation + } + try { + if (toTokenInfoForToken == null) { + const tokenId = await getTokenId(request.toWallet, info.address) + if ( + info.chain_id === + EDGE_PLUGINID_CHAINID_MAP[ + request.toWallet.currencyInfo.pluginId + ] && + tokenId === request.toTokenId + ) { + toTokenInfoForToken = info + } } - } catch (e) { + } catch (e: unknown) { // ignore tokens that fail validation } } @@ -213,14 +250,13 @@ export function makeBridgelessPlugin( throw new SwapCurrencyError(swapInfo, request) } + const fromDecimals = parseInt(fromTokenInfo.decimals, 10) + const toDecimals = parseInt(toTokenInfo.decimals, 10) + // The minimum amount is enforced by the amount of toToken received let fromAmount: string let toAmount: string if (request.quoteFor === 'to') { - fromAmount = ceil( - mul(request.nativeAmount, add('1', toTokenInfo.commission_rate)), - 0 - ) toAmount = request.nativeAmount if (lt(toAmount, toTokenInfo.min_withdrawal_amount)) { @@ -230,20 +266,43 @@ export function makeBridgelessPlugin( 'to' ) } + + const grossToAmount = ceil( + mul(toAmount, add('1', toTokenInfo.commission_rate)), + 0 + ) + fromAmount = scaleNativeAmount( + grossToAmount, + toDecimals, + fromDecimals, + 'up' + ) } else { fromAmount = request.nativeAmount + const bridgedToAmount = scaleNativeAmount( + fromAmount, + fromDecimals, + toDecimals, + 'down' + ) toAmount = ceil( - mul(request.nativeAmount, sub('1', toTokenInfo.commission_rate)), + mul(bridgedToAmount, sub('1', toTokenInfo.commission_rate)), 0 ) - const minFromAmount = ceil( + const minGrossToAmount = ceil( mul( toTokenInfo.min_withdrawal_amount, add('1', toTokenInfo.commission_rate) ), 0 ) + const minFromAmount = scaleNativeAmount( + minGrossToAmount, + toDecimals, + fromDecimals, + 'up' + ) if (lt(toAmount, toTokenInfo.min_withdrawal_amount)) { throw new SwapBelowLimitError(swapInfo, minFromAmount, 'from') } diff --git a/test/utils.test.ts b/test/utils.test.ts index 823f6ad6..3d182dd4 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,6 +1,7 @@ import { assert } from 'chai' import { describe, it } from 'mocha' +import { scaleNativeAmount } from '../src/swap/defi/bridgeless' import { makeQueryParams } from '../src/util/utils' describe(`makeQueryParams`, function () { @@ -17,3 +18,36 @@ describe(`makeQueryParams`, function () { ) }) }) + +describe(`scaleNativeAmount`, function () { + it('returns the same amount when decimals match', function () { + assert.equal(scaleNativeAmount('12345', 6, 6, 'down'), '12345') + }) + + it('expands amount when destination has more decimals', function () { + assert.equal(scaleNativeAmount('12345', 6, 18, 'down'), '12345000000000000') + }) + + it('truncates amount when destination has fewer decimals', function () { + assert.equal( + scaleNativeAmount('20005000000000000000', 18, 6, 'down'), + '20005000' + ) + }) + + it('rounds up when truncated remainder is non-zero', function () { + assert.equal(scaleNativeAmount('1234567', 6, 3, 'up'), '1235') + }) + + it('does not round up when truncated remainder is zero', function () { + assert.equal(scaleNativeAmount('1234000', 6, 3, 'up'), '1234') + }) + + it('returns one when rounding up a tiny amount below precision', function () { + assert.equal(scaleNativeAmount('1', 18, 6, 'up'), '1') + }) + + it('returns zero when rounding down a tiny amount below precision', function () { + assert.equal(scaleNativeAmount('1', 18, 6, 'down'), '0') + }) +}) From 27155010812fa8df0bf8b36e4d0fb0900f6517b4 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 26 Mar 2026 13:53:42 -0700 Subject: [PATCH 3/4] Add ethereum and binance smart chain support --- src/swap/defi/bridgeless.ts | 100 ++++++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 5 deletions(-) diff --git a/src/swap/defi/bridgeless.ts b/src/swap/defi/bridgeless.ts index 47fbcfec..193e6e84 100644 --- a/src/swap/defi/bridgeless.ts +++ b/src/swap/defi/bridgeless.ts @@ -27,6 +27,7 @@ import { SwapBelowLimitError, SwapCurrencyError } from 'edge-core-js/types' +import { ethers } from 'ethers' import { getMaxSwappable, @@ -35,6 +36,7 @@ import { } from '../../util/swapHelpers' import { convertRequest, getAddress } from '../../util/utils' import { EdgeSwapRequestPlugin, MakeTxParams } from '../types' +import { createEvmApprovalEdgeTransactions } from './defiUtils' const pluginId = 'bridgeless' @@ -51,8 +53,11 @@ const AUTO_BOT_URL = 'https://autobot-wusa1.edge.app' const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' const EDGE_PLUGINID_CHAINID_MAP: Record = { + // base: '8453', // no swaps available on zano yet + binancesmartchain: '56', bitcoin: '0', bitcoincash: '5', + ethereum: '1', zano: '2' } @@ -141,6 +146,45 @@ export const scaleNativeAmount = ( return whole } +const BRIDGELESS_EVM_ABI = [ + 'function depositERC20(address token_, uint256 amount_, string receiver_, string network_, bool isWrapped_, uint16 referralId_)', + 'function depositNative(string receiver_, string network_, uint16 referralId_) payable' +] + +interface BridgelessTokenInfo { + address: string + is_wrapped: boolean +} + +interface MakeBridgelessEvmSpendInfoParams { + fromAmount: string + fromTokenInfo: BridgelessTokenInfo + toChainId: string + receiver: string +} + +const makeBridgelessEvmSpendInfo = ({ + fromAmount, + fromTokenInfo, + toChainId, + receiver +}: MakeBridgelessEvmSpendInfoParams): string => { + const iface = new ethers.utils.Interface(BRIDGELESS_EVM_ABI) + const tokenAddress = ethers.utils.getAddress(fromTokenInfo.address) + const isNativeToken = tokenAddress.toLowerCase() === ZERO_ADDRESS + + return isNativeToken + ? iface.encodeFunctionData('depositNative', [receiver, toChainId, 0]) + : iface.encodeFunctionData('depositERC20', [ + tokenAddress, + fromAmount, + receiver, + toChainId, + fromTokenInfo.is_wrapped, + 0 + ]) +} + export function makeBridgelessPlugin( opts: EdgeCorePluginOptions ): EdgeSwapPlugin { @@ -308,11 +352,6 @@ export function makeBridgelessPlugin( } } - const receiver = - request.toWallet.currencyInfo.pluginId === 'zano' - ? base16.encode(base58.decode(toAddress)) - : toAddress - // chainId/txid/outputIndex // output index is 0 for both Bitcoin (output of actual deposit) and Zano (index of serviceEntries with deposit instructions) // txid must be 0x prefixed @@ -344,6 +383,10 @@ export function makeBridgelessPlugin( switch (request.fromWallet.currencyInfo.pluginId) { case 'bitcoin': case 'bitcoincash': { + const receiver = + request.toWallet.currencyInfo.pluginId === 'zano' + ? base16.encode(base58.decode(toAddress)) + : toAddress const opReturn = `${receiver}${Buffer.from( `#${toChainId}`, 'utf8' @@ -373,6 +416,53 @@ export function makeBridgelessPlugin( fromNativeAmount: fromAmount } } + case 'binancesmartchain': + case 'ethereum': { + const data = makeBridgelessEvmSpendInfo({ + fromAmount, + fromTokenInfo, + toChainId, + receiver: toAddress + }) + const isNativeToken = + request.fromTokenId == null || + fromTokenInfo.address.toLowerCase() === ZERO_ADDRESS + const preTxs = isNativeToken + ? [] + : await createEvmApprovalEdgeTransactions({ + request, + approvalAmount: fromAmount, + tokenContractAddress: fromTokenInfo.address, + recipientAddress: bridgeAddress + }) + + // For tokens, the nonce is the second log emitted from the transaction. + if (!isNativeToken) { + const orderId = `${fromChainId}/{{TXID}}/1` + savedAction.orderId = orderId + savedAction.orderUri = `${ORDER_URL}/check/${orderId}` + } + const spendInfo: EdgeSpendInfo = { + tokenId: request.fromTokenId, + spendTargets: [ + { + nativeAmount: fromAmount, + publicAddress: bridgeAddress + } + ], + memos: [{ type: 'hex', value: data.replace(/^0x/, '') }], + assetAction, + savedAction + } + + return { + request, + spendInfo, + preTxs, + swapInfo, + fromNativeAmount: fromAmount + } + } case 'zano': { const bodyData = { dst_add: toAddress, From 96f67600a5c81fd25125d103bc9dc2732c21f5d1 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 26 Mar 2026 16:22:01 -0700 Subject: [PATCH 4/4] Limit bridgeless plugin to Zano-related swaps This is an arbitrary limitation. Other swaps should work (ie. bitcoin to ethereum btcx token) but they are untested. # Conflicts: # CHANGELOG.md --- CHANGELOG.md | 2 ++ src/swap/defi/bridgeless.ts | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a27992f4..5b88a4dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- added: Support Bitcoin Cash, Ethereum, and BNB Chain in Bridgeless swap plugin + ## 2.44.0 (2026-03-28) - added: (Rango) SUI support diff --git a/src/swap/defi/bridgeless.ts b/src/swap/defi/bridgeless.ts index 193e6e84..61dd0e09 100644 --- a/src/swap/defi/bridgeless.ts +++ b/src/swap/defi/bridgeless.ts @@ -197,7 +197,12 @@ export function makeBridgelessPlugin( EDGE_PLUGINID_CHAINID_MAP[request.fromWallet.currencyInfo.pluginId] const toChainId = EDGE_PLUGINID_CHAINID_MAP[request.toWallet.currencyInfo.pluginId] - if (fromChainId == null || toChainId == null || fromChainId === toChainId) { + if ( + fromChainId == null || + toChainId == null || + fromChainId === toChainId || + (fromChainId !== '2' && toChainId !== '2') // Only use this plugin for swaps that involve the Zano blockchain + ) { throw new SwapCurrencyError(swapInfo, request) }