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: Arc swaps via LI.FI

## 2.56.0 (2026-09-16)

- added: (Rango) Tron swaps
Expand Down
3 changes: 3 additions & 0 deletions scripts/mappings/lifiMappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ lifi.set('ape', null)
// Display Name: Arbitrum
lifi.set('arb', 'arbitrum')

// Display Name: Arc
lifi.set('arc', 'arc')

// Display Name: Aurora
lifi.set('aur', null)

Expand Down
1 change: 1 addition & 0 deletions src/mappings/changehero.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ changehero.set('abstract', null)
changehero.set('algorand', 'algorand')
changehero.set('amoy', null)
changehero.set('arbitrum', 'arbitrum')
changehero.set('arc', null)
changehero.set('avalanche', 'avalanche_(c-chain)')
changehero.set('axelar', null)
changehero.set('badcoin', null)
Expand Down
1 change: 1 addition & 0 deletions src/mappings/changelly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ changelly.set('abstract', null)
changelly.set('algorand', 'algorand')
changelly.set('amoy', null)
changelly.set('arbitrum', 'arbitrum')
changelly.set('arc', null)
changelly.set('avalanche', 'avaxc')
changelly.set('axelar', null)
changelly.set('badcoin', null)
Expand Down
1 change: 1 addition & 0 deletions src/mappings/changenow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ changenow.set('abstract', null)
changenow.set('algorand', 'algo')
changenow.set('amoy', null)
changenow.set('arbitrum', 'arbitrum')
changenow.set('arc', null)
changenow.set('avalanche', 'avaxc')
changenow.set('axelar', 'axl')
changenow.set('badcoin', null)
Expand Down
1 change: 1 addition & 0 deletions src/mappings/exolix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ exolix.set('abstract', null)
exolix.set('algorand', 'ALGO')
exolix.set('amoy', null)
exolix.set('arbitrum', 'ARBITRUM')
exolix.set('arc', null)
exolix.set('avalanche', 'AVAXC')
exolix.set('axelar', null)
exolix.set('badcoin', null)
Expand Down
1 change: 1 addition & 0 deletions src/mappings/godex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ godex.set('abstract', null)
godex.set('algorand', 'ALGO')
godex.set('amoy', null)
godex.set('arbitrum', 'ARBITRUM')
godex.set('arc', null)
godex.set('avalanche', 'AVAXC')
godex.set('axelar', 'WAXL')
godex.set('badcoin', null)
Expand Down
1 change: 1 addition & 0 deletions src/mappings/letsexchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ letsexchange.set('abstract', null)
letsexchange.set('algorand', 'ALGO')
letsexchange.set('amoy', null)
letsexchange.set('arbitrum', 'ARBITRUM')
letsexchange.set('arc', null)
letsexchange.set('avalanche', 'AVAXC')
letsexchange.set('axelar', 'WAXL')
letsexchange.set('badcoin', null)
Expand Down
1 change: 1 addition & 0 deletions src/mappings/lifi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ lifi.set('abstract', null)
lifi.set('algorand', null)
lifi.set('amoy', null)
lifi.set('arbitrum', 'arb')
lifi.set('arc', 'arc')
lifi.set('avalanche', 'ava')
lifi.set('axelar', null)
lifi.set('badcoin', null)
Expand Down
1 change: 1 addition & 0 deletions src/mappings/mayaprotocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mayaprotocol.set('abstract', null)
mayaprotocol.set('algorand', null)
mayaprotocol.set('amoy', null)
mayaprotocol.set('arbitrum', 'ARB')
mayaprotocol.set('arc', null)
mayaprotocol.set('avalanche', null)
mayaprotocol.set('axelar', null)
mayaprotocol.set('badcoin', null)
Expand Down
1 change: 1 addition & 0 deletions src/mappings/nexchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ nexchange.set('abstract', null)
nexchange.set('algorand', 'ALGO')
nexchange.set('amoy', null)
nexchange.set('arbitrum', 'ARB')
nexchange.set('arc', null)
nexchange.set('avalanche', 'AVAXC')
nexchange.set('axelar', null)
nexchange.set('badcoin', null)
Expand Down
1 change: 1 addition & 0 deletions src/mappings/rango.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ rango.set('abstract', null)
rango.set('algorand', null)
rango.set('amoy', null)
rango.set('arbitrum', 'ARBITRUM')
rango.set('arc', null)
rango.set('avalanche', 'AVAX_CCHAIN')
rango.set('axelar', 'AXELAR')
rango.set('badcoin', null)
Expand Down
1 change: 1 addition & 0 deletions src/mappings/sideshift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ sideshift.set('abstract', null)
sideshift.set('algorand', 'algorand')
sideshift.set('amoy', null)
sideshift.set('arbitrum', 'arbitrum')
sideshift.set('arc', null)
sideshift.set('avalanche', 'avax')
sideshift.set('axelar', null)
sideshift.set('badcoin', null)
Expand Down
1 change: 1 addition & 0 deletions src/mappings/swapkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ swapkit.set('abstract', null)
swapkit.set('algorand', null)
swapkit.set('amoy', null)
swapkit.set('arbitrum', 'ARB')
swapkit.set('arc', null)
swapkit.set('avalanche', 'AVAX')
swapkit.set('axelar', null)
swapkit.set('badcoin', null)
Expand Down
1 change: 1 addition & 0 deletions src/mappings/swapuz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ swapuz.set('abstract', null)
swapuz.set('algorand', 'ALGO')
swapuz.set('amoy', null)
swapuz.set('arbitrum', 'ARB')
swapuz.set('arc', null)
swapuz.set('avalanche', 'AVAX')
swapuz.set('axelar', null)
swapuz.set('badcoin', null)
Expand Down
1 change: 1 addition & 0 deletions src/mappings/thorchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ thorchain.set('abstract', null)
thorchain.set('algorand', null)
thorchain.set('amoy', null)
thorchain.set('arbitrum', null)
thorchain.set('arc', null)
thorchain.set('avalanche', 'AVAX')
thorchain.set('axelar', null)
thorchain.set('badcoin', null)
Expand Down
1 change: 1 addition & 0 deletions src/mappings/xgram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ xgram.set('abstract', null)
xgram.set('algorand', 'ALGO')
xgram.set('amoy', null)
xgram.set('arbitrum', 'ARBITRUM')
xgram.set('arc', null)
xgram.set('avalanche', 'AVAXC')
xgram.set('axelar', null)
xgram.set('badcoin', null)
Expand Down
134 changes: 120 additions & 14 deletions src/swap/defi/lifi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mul, round } from 'biggystring'
import { add, div, gt, lt, mul, round } from 'biggystring'
import {
asArray,
asNumber,
Expand All @@ -15,6 +15,7 @@ import {
EdgeSwapQuote,
EdgeSwapRequest,
EdgeTransaction,
InsufficientFundsError,
SwapBelowLimitError,
SwapCurrencyError
} from 'edge-core-js/types'
Expand Down Expand Up @@ -68,8 +69,41 @@ const LIFI_SERVERS_DEFAULT = ['https://li.quest']
const EXPIRATION_MS = 1000 * 60
const EXCHANGE_INFO_UPDATE_FREQ_MS = 60000

/**
* Chains whose native asset LI.FI trades as an ERC-20 interface to the native
* balance, at the interface's precision. On Arc, LI.FI's native token is USDC
* at 0x3600…0000 with 6 decimals, while the wallet counts the same balance in
* 18, and a swap from it is an approval plus a call that sends no value.
*/
const NATIVE_ERC20_INTERFACES: {
[pluginId: string]: { contractAddress: string; multiplier: string }
} = {
arc: {
contractAddress: '0x3600000000000000000000000000000000000000',
multiplier: '1000000'
}
}

/**
* The factor from LI.FI's units to the wallet's native units, when LI.FI
* trades this wallet's native asset through an ERC-20 interface.
*/
const getNativeInterfaceScale = (
currencyInfo: EdgeSwapRequestPlugin['fromWallet']['currencyInfo']
): string | undefined => {
const nativeInterface = NATIVE_ERC20_INTERFACES[currencyInfo.pluginId]
if (nativeInterface == null) return
return div(
currencyInfo.denominations[0].multiplier,
nativeInterface.multiplier
)
}

// https://li.quest/v1/chains
const getParentTokenContractAddress = (pluginId: string): string => {
const nativeInterface = NATIVE_ERC20_INTERFACES[pluginId]
if (nativeInterface != null) return nativeInterface.contractAddress

switch (pluginId) {
// chainType UTXO
case 'bitcoin': {
Expand Down Expand Up @@ -218,8 +252,13 @@ export function makeLifiPlugin(opts: EdgeCorePluginOptions): EdgeSwapPlugin {
'Content-Type': 'application/json'
}

/**
* `fromMaxSwap` skips the plugin's own balance check on an interface-traded
* native asset: a max request sizes itself from this quote's fees.
*/
const fetchSwapQuoteInner = async (
request: EdgeSwapRequestPlugin
request: EdgeSwapRequestPlugin,
fromMaxSwap: boolean = false
): Promise<SwapOrder> => {
const {
fromCurrencyCode,
Expand All @@ -238,21 +277,27 @@ export function makeLifiPlugin(opts: EdgeCorePluginOptions): EdgeSwapPlugin {
const fromToken = fromWallet.currencyConfig.allTokens[fromTokenId ?? '']
let fromContractAddress
let sendingToken = false
// Set when LI.FI trades the source's native asset as an ERC-20 interface:
let fromNativeScale: string | undefined
if (fromCurrencyCode === fromWallet.currencyInfo.currencyCode) {
fromContractAddress = getParentTokenContractAddress(
fromWallet.currencyInfo.pluginId
)
fromNativeScale = getNativeInterfaceScale(fromWallet.currencyInfo)
if (fromNativeScale != null) sendingToken = true
} else {
sendingToken = true
fromContractAddress = fromToken?.networkLocation?.contractAddress
}

const toToken = toWallet.currencyConfig.allTokens[toTokenId ?? '']
let toContractAddress
let toNativeScale: string | undefined
if (toCurrencyCode === toWallet.currencyInfo.currencyCode) {
toContractAddress = getParentTokenContractAddress(
toWallet.currencyInfo.pluginId
)
toNativeScale = getNativeInterfaceScale(toWallet.currencyInfo)
} else {
toContractAddress = toToken?.networkLocation?.contractAddress
}
Expand Down Expand Up @@ -324,12 +369,22 @@ export function makeLifiPlugin(opts: EdgeCorePluginOptions): EdgeSwapPlugin {
slippage = lifi?.slippage
}

// LI.FI counts an interface-traded native asset at the interface's
// precision, so the request drops the digits it cannot express:
const lifiFromAmount =
fromNativeScale == null
? nativeAmount
: div(nativeAmount, fromNativeScale)
if (lifiFromAmount === '0') {
throw new SwapBelowLimitError(swapInfo, undefined, 'from')
}

const params = makeQueryParams({
fromChain: fromMainnetCode,
toChain: toMainnetCode,
fromToken: fromContractAddress,
toToken: toContractAddress,
fromAmount: nativeAmount,
fromAmount: lifiFromAmount,
fromAddress,
toAddress,
integrator,
Expand Down Expand Up @@ -367,7 +422,29 @@ export function makeLifiPlugin(opts: EdgeCorePluginOptions): EdgeSwapPlugin {
const providers = includedSteps.map(s => s.toolDetails.name)
const providersStr = providers.join(' -> ')
const metadataNotes = `DEX Providers: ${providersStr}`
const { approvalAddress, toAmount, toAmountMin, fromAmount } = estimate
const { approvalAddress } = estimate
// The approval below takes LI.FI's amount, and the interface contract
// pulls against it with no value in the call, so a quote spending more
// than was asked would take more than the user agreed to:
if (fromNativeScale != null && gt(estimate.fromAmount, lifiFromAmount)) {
throw new Error(
`LI.FI quoted ${estimate.fromAmount}, above the requested ${lifiFromAmount}`
)
}
// Everything below is in wallet units. `estimate.fromAmount` stays in
// LI.FI's units for the approval, which the interface contract reads.
const fromAmount =
fromNativeScale == null
? estimate.fromAmount
: mul(estimate.fromAmount, fromNativeScale)
const toAmount =
toNativeScale == null
? estimate.toAmount
: mul(estimate.toAmount, toNativeScale)
const toAmountMin =
toNativeScale == null
? estimate.toAmountMin
: mul(estimate.toAmountMin, toNativeScale)

const preTxs: EdgeTransaction[] = []
let spendInfo: EdgeSpendInfo
Expand Down Expand Up @@ -469,33 +546,62 @@ export function makeLifiPlugin(opts: EdgeCorePluginOptions): EdgeSwapPlugin {
const gasPriceDecimal = hexToDecimal(gasPrice)
const gasPriceGwei = bufferSwapGasPrice(gasPriceDecimal)

// XXX Hack. Lifi doesn't properly estimate ethereum gas limits. Increase by 40%
const swapGasLimit = round(mul(hexToDecimal(gasLimit), '1.4'), 0)

if (sendingToken) {
const approvalTxs = await createEvmApprovalEdgeTransactions({
request,
approvalAmount: fromAmount,
approvalAmount:
fromNativeScale == null ? fromAmount : estimate.fromAmount,
Comment thread
j0ntz marked this conversation as resolved.
tokenContractAddress: fromContractAddress,
recipientAddress: approvalAddress,
networkFeeOption: 'custom',
customNetworkFee: {
gasPrice: gasPriceGwei
}
})
// The approval's own amount is in the interface's units, which the
// wallet would show as a sliver of its native asset:
for (const approvalTx of approvalTxs) {
const { savedAction } = approvalTx
if (
fromNativeScale != null &&
savedAction?.actionType === 'tokenApproval'
) {
savedAction.tokenApproved.nativeAmount = fromAmount
}
}
preTxs.push(...approvalTxs)
}

if (fromNativeScale != null && !fromMaxSwap) {
// The call sends no value, so the engine's balance check covers
// only its fee while the interface contract spends the balance.
// The swap must leave room for itself and for the approval fees.
const swapFee = mul(swapGasLimit, mul(gasPriceGwei, '1000000000'))
const fees = preTxs.reduce(
(sum, preTx) => add(sum, preTx.networkFee),
swapFee
)
const balance = fromWallet.balanceMap.get(null) ?? '0'
if (lt(balance, add(fromAmount, fees))) {
throw new InsufficientFundsError({ tokenId: null })
}
}

spendInfo = {
tokenId: request.fromTokenId,
spendTargets: [
{
nativeAmount: fromAmount,
nativeAmount: fromNativeScale == null ? fromAmount : '0',
publicAddress: approvalAddress
}
],
memos: [{ type: 'hex', value: data.replace(/^0x/, '') }],
networkFeeOption: 'custom',
customNetworkFee: {
// XXX Hack. Lifi doesn't properly estimate ethereum gas limits. Increase by 40%
gasLimit: round(mul(hexToDecimal(gasLimit), '1.4'), 0),
gasLimit: swapGasLimit,
gasPrice: gasPriceGwei
},
assetAction: {
Expand Down Expand Up @@ -525,7 +631,7 @@ export function makeLifiPlugin(opts: EdgeCorePluginOptions): EdgeSwapPlugin {

return {
expirationDate: new Date(Date.now() + EXPIRATION_MS),
fromNativeAmount: nativeAmount,
fromNativeAmount: fromNativeScale == null ? nativeAmount : fromAmount,
metadataNotes,
minReceiveAmount: toAmountMin,
preTxs,
Expand All @@ -552,13 +658,13 @@ export function makeLifiPlugin(opts: EdgeCorePluginOptions): EdgeSwapPlugin {
quoteFor: 'from'
}
} else {
newRequest = await getMaxSwappable(
async r => await fetchSwapQuoteInner(r),
request
)
newRequest = await getMaxSwappable(fetchSwapQuoteInner, request, true)
}
}
const swapOrder = await fetchSwapQuoteInner(newRequest)
const swapOrder = await fetchSwapQuoteInner(
newRequest,
request.quoteFor === 'max'
)
return await makeSwapPluginQuote(swapOrder)
}
}
Expand Down
1 change: 1 addition & 0 deletions src/util/edgeCurrencyPluginIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const edgeCurrencyPluginIds = [
'algorand',
'amoy',
'arbitrum',
'arc',
'avalanche',
'axelar',
'badcoin',
Expand Down
Loading