Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: release v1.650.0 #7445

Merged
merged 5 commits into from
Jul 31, 2024
Merged
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
3 changes: 3 additions & 0 deletions .env.base
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ REACT_APP_FEATURE_ARBITRUM_BRIDGE=false
REACT_APP_FEATURE_CUSTOM_TOKEN_IMPORT=true
REACT_APP_FEATURE_ARBITRUM_BRIDGE_CLAIMS=false
REACT_APP_FEATURE_USDT_APPROVAL_RESET=false
REACT_APP_FEATURE_PORTALS_SWAPPER=false
REACT_APP_FEATURE_RUNEPOOL=false

# absolute URL prefix
REACT_APP_ABSOLUTE_URL_PREFIX=https://app.shapeshift.com
Expand Down Expand Up @@ -166,6 +168,7 @@ REACT_APP_FEATURE_COVALENT_JAYPEGS=true

# Portals

REACT_APP_PORTALS_BASE_URL=https://api.portals.fi
REACT_APP_PORTALS_API_KEY=bbc3ba7e-5f2a-4a0a-bbbc-22509944686c

#oneinch
Expand Down
2 changes: 2 additions & 0 deletions .env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ REACT_APP_FEATURE_RFOX_REWARDS_TX_HISTORY=true
REACT_APP_FEATURE_ARBITRUM_BRIDGE=true
REACT_APP_FEATURE_ARBITRUM_BRIDGE_CLAIMS=true
REACT_APP_FEATURE_USDT_APPROVAL_RESET=true
REACT_APP_FEATURE_RUNEPOOL=true
REACT_APP_FEATURE_PORTALS_SWAPPER=true

# logging
REACT_APP_REDUX_WINDOW=false
Expand Down
2 changes: 2 additions & 0 deletions .env.develop
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ REACT_APP_FEATURE_RFOX_REWARDS_TX_HISTORY=true
REACT_APP_FEATURE_CHATWOOT=true
REACT_APP_FEATURE_ARBITRUM_BRIDGE=true
REACT_APP_FEATURE_USDT_APPROVAL_RESET=true
REACT_APP_FEATURE_RUNEPOOL=true
REACT_APP_FEATURE_PORTALS_SWAPPER=true

# mixpanel
REACT_APP_MIXPANEL_TOKEN=1c1369f6ea23a6404bac41b42817cc4b
Expand Down
10 changes: 10 additions & 0 deletions packages/swapper/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { LIFI_SUPPORTED_CHAIN_IDS } from './swappers/LifiSwapper/utils/constants
import { oneInchApi } from './swappers/OneInchSwapper/endpoints'
import { oneInchSwapper } from './swappers/OneInchSwapper/OneInchSwapper'
import { ONE_INCH_SUPPORTED_CHAIN_IDS } from './swappers/OneInchSwapper/utils/constants'
import { PORTALS_SUPPORTED_CHAIN_IDS } from './swappers/PortalsSwapper/constants'
import { portalsApi } from './swappers/PortalsSwapper/endpoints'
import { portalsSwapper } from './swappers/PortalsSwapper/PortalsSwapper'
import { THORCHAIN_SUPPORTED_CHAIN_IDS } from './swappers/ThorchainSwapper/constants'
import { thorchainApi } from './swappers/ThorchainSwapper/endpoints'
import { thorchainSwapper } from './swappers/ThorchainSwapper/ThorchainSwapper'
Expand Down Expand Up @@ -76,6 +79,12 @@ export const swappers: Record<
supportedChainIds: ARBITRUM_BRIDGE_SUPPORTED_CHAIN_IDS,
pollingInterval: DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL,
},
[SwapperName.Portals]: {
...portalsSwapper,
...portalsApi,
supportedChainIds: PORTALS_SUPPORTED_CHAIN_IDS,
pollingInterval: DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL,
},
[SwapperName.Test]: undefined,
}

Expand All @@ -93,6 +102,7 @@ export const getDefaultSlippageDecimalPercentageForSwapper = (
switch (swapperName) {
case SwapperName.Zrx:
case SwapperName.OneInch:
case SwapperName.Portals:
case SwapperName.Test:
return DEFAULT_SLIPPAGE_DECIMAL_PERCENTAGE
case SwapperName.LIFI:
Expand Down
26 changes: 26 additions & 0 deletions packages/swapper/src/swappers/PortalsSwapper/PortalsSwapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { AssetId } from '@shapeshiftoss/caip'
import type { Asset } from '@shapeshiftoss/types'

import type { BuyAssetBySellIdInput, Swapper } from '../../types'
import { executeEvmTransaction } from '../../utils'
import { PORTALS_SUPPORTED_CHAIN_IDS } from './constants'

export const portalsSwapper: Swapper = {
executeEvmTransaction,

filterAssetIdsBySellable: (assets: Asset[]): Promise<AssetId[]> => {
return Promise.resolve(
assets
.filter(asset => PORTALS_SUPPORTED_CHAIN_IDS.sell.includes(asset.chainId))
.map(asset => asset.assetId),
)
},

filterBuyAssetsBySellAssetId: (input: BuyAssetBySellIdInput): Promise<AssetId[]> => {
return Promise.resolve(
input.assets
.filter(asset => PORTALS_SUPPORTED_CHAIN_IDS.buy.includes(asset.chainId))
.map(asset => asset.assetId),
)
},
}
21 changes: 21 additions & 0 deletions packages/swapper/src/swappers/PortalsSwapper/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { ChainId } from '@shapeshiftoss/caip'
import { KnownChainIds } from '@shapeshiftoss/types'

import type { SupportedChainIds } from '../../types'
import { PortalsSupportedChainIds } from './types'

export const PORTALS_SUPPORTED_CHAIN_IDS: SupportedChainIds = {
sell: PortalsSupportedChainIds as unknown as ChainId[],
buy: PortalsSupportedChainIds as unknown as ChainId[],
}

export const chainIdToPortalsNetwork: Partial<Record<KnownChainIds, string>> = {
[KnownChainIds.EthereumMainnet]: 'ethereum',
[KnownChainIds.AvalancheMainnet]: 'avalanche',
[KnownChainIds.OptimismMainnet]: 'optimism',
[KnownChainIds.BnbSmartChainMainnet]: 'bsc',
[KnownChainIds.PolygonMainnet]: 'polygon',
[KnownChainIds.GnosisMainnet]: 'gnosis',
[KnownChainIds.ArbitrumMainnet]: 'arbitrum',
[KnownChainIds.BaseMainnet]: 'base',
}
41 changes: 41 additions & 0 deletions packages/swapper/src/swappers/PortalsSwapper/endpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Result } from '@sniptt/monads/build'

import type {
EvmTransactionRequest,
GetEvmTradeQuoteInput,
GetTradeQuoteInput,
GetUnsignedEvmTransactionArgs,
SwapErrorRight,
SwapperApi,
SwapperDeps,
TradeQuote,
} from '../../types'
import { checkEvmSwapStatus } from '../../utils'
import { getPortalsTradeQuote } from './getPortalsTradeQuote/getPortalsTradeQuote'

export const portalsApi: SwapperApi = {
getTradeQuote: async (
input: GetTradeQuoteInput,
{ config, assertGetEvmChainAdapter }: SwapperDeps,
): Promise<Result<TradeQuote[], SwapErrorRight>> => {
const tradeQuoteResult = await getPortalsTradeQuote(
input as GetEvmTradeQuoteInput,
assertGetEvmChainAdapter,
config,
)

return tradeQuoteResult.map(tradeQuote => {
return [tradeQuote]
})
},

// @ts-ignore TODO(gomes): implement me
getUnsignedEvmTransaction: async ({
chainId,
from,
tradeQuote,
supportsEIP1559, // @ts-ignore TODO(gomes): implement me
}: GetUnsignedEvmTransactionArgs): Promise<EvmTransactionRequest> => {},

checkTradeStatus: checkEvmSwapStatus,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import type { ChainId } from '@shapeshiftoss/caip'
import { fromAssetId } from '@shapeshiftoss/caip'
import type { EvmChainAdapter } from '@shapeshiftoss/chain-adapters'
import type { KnownChainIds } from '@shapeshiftoss/types'
import { bn, bnOrZero, convertBasisPointsToDecimalPercentage } from '@shapeshiftoss/utils'
import { calcNetworkFeeCryptoBaseUnit } from '@shapeshiftoss/utils/dist/evm'
import type { Result } from '@sniptt/monads'
import { Err, Ok } from '@sniptt/monads'
import { zeroAddress } from 'viem'

import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants'
import type { SwapperConfig } from '../../../types'
import {
type GetEvmTradeQuoteInput,
type SingleHopTradeQuoteSteps,
type SwapErrorRight,
SwapperName,
type TradeQuote,
TradeQuoteError,
} from '../../../types'
import { makeSwapErrorRight } from '../../../utils'
import { getTreasuryAddressFromChainId, isNativeEvmAsset } from '../../utils/helpers/helpers'
import { chainIdToPortalsNetwork } from '../constants'
import { fetchPortalsTradeOrder } from '../utils/fetchPortalsTradeOrder'
import { isSupportedChainId } from '../utils/helpers'

export async function getPortalsTradeQuote(
input: GetEvmTradeQuoteInput,
assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter,
swapperConfig: SwapperConfig,
): Promise<Result<TradeQuote, SwapErrorRight>> {
const {
sellAsset,
buyAsset,
sendAddress,
accountNumber,
affiliateBps,
potentialAffiliateBps,
chainId,
supportsEIP1559,
sellAmountIncludingProtocolFeesCryptoBaseUnit,
} = input

const sellAssetChainId = sellAsset.chainId
const buyAssetChainId = buyAsset.chainId

if (!isSupportedChainId(sellAssetChainId)) {
return Err(
makeSwapErrorRight({
message: `unsupported chainId`,
code: TradeQuoteError.UnsupportedChain,
details: { chainId: sellAsset.chainId },
}),
)
}

if (!isSupportedChainId(buyAssetChainId)) {
return Err(
makeSwapErrorRight({
message: `unsupported chainId`,
code: TradeQuoteError.UnsupportedChain,
details: { chainId: sellAsset.chainId },
}),
)
}

if (sellAssetChainId !== buyAssetChainId) {
return Err(
makeSwapErrorRight({
message: `cross-chain not supported - both assets must be on chainId ${sellAsset.chainId}`,
code: TradeQuoteError.CrossChainNotSupported,
details: { buyAsset, sellAsset },
}),
)
}

// Not a decimal percentage, just a good ol' percentage e.g 1 for 1%
const affiliateBpsPercentage = convertBasisPointsToDecimalPercentage(affiliateBps)
.times(100)
.toNumber()

const slippageTolerancePercentageDecimal =
input.slippageTolerancePercentageDecimal ??
getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Portals)

try {
if (!sendAddress) return Err(makeSwapErrorRight({ message: 'missing sendAddress' }))

const portalsNetwork = chainIdToPortalsNetwork[chainId as KnownChainIds]

if (!portalsNetwork) {
return Err(
makeSwapErrorRight({
message: `unsupported ChainId`,
code: TradeQuoteError.UnsupportedChain,
details: { chainId: input.chainId },
}),
)
}

const sellAssetAddress = isNativeEvmAsset(sellAsset.assetId)
? zeroAddress
: fromAssetId(sellAsset.assetId).assetReference
const buyAssetAddress = isNativeEvmAsset(buyAsset.assetId)
? zeroAddress
: fromAssetId(buyAsset.assetId).assetReference

const inputToken = `${portalsNetwork}:${sellAssetAddress}`
const outputToken = `${portalsNetwork}:${buyAssetAddress}`

// Attempt fetching a quote with validation enabled to leverage upstream gasLimit estimate
const portalsTradeOrderResponse = await fetchPortalsTradeOrder({
sender: sendAddress,
inputToken,
outputToken,
inputAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit,
slippageTolerancePercentage: Number(slippageTolerancePercentageDecimal) * 100,
partner: getTreasuryAddressFromChainId(sellAsset.chainId),
feePercentage: affiliateBpsPercentage,
validate: true,
swapperConfig,
}).catch(e => {
console.info('failed to get Portals quote with validation enabled', e)

// If validation fails, try again without validation, we won't get network fees, but we can't do any better
return fetchPortalsTradeOrder({
sender: sendAddress,
inputToken,
outputToken,
inputAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit,
slippageTolerancePercentage: Number(slippageTolerancePercentageDecimal) * 100,
partner: getTreasuryAddressFromChainId(sellAsset.chainId),
feePercentage: affiliateBpsPercentage,
validate: false,
swapperConfig,
})
})

const {
context: {
orderId,
outputAmount: buyAmountAfterFeesCryptoBaseUnit,
minOutputAmount: buyAmountBeforeFeesCryptoBaseUnit,
slippageTolerancePercentage,
target: allowanceContract,
feeAmount,
gasLimit,
},
} = portalsTradeOrderResponse

const rate = bn(buyAmountAfterFeesCryptoBaseUnit)
.div(input.sellAmountIncludingProtocolFeesCryptoBaseUnit)
.toString()

const adapter = assertGetEvmChainAdapter(chainId)
const { average } = await adapter.getGasFeeData()

const networkFeeCryptoBaseUnit = calcNetworkFeeCryptoBaseUnit({
...average,
supportsEIP1559,
// times 1 isn't a mistake, it's just so we can write this comment above to mention that Portals already add a
// buffer of ~15% to the gas limit
gasLimit: bnOrZero(gasLimit).times(1).toFixed(),
})

const tradeQuote: TradeQuote = {
id: orderId,
receiveAddress: input.receiveAddress,
affiliateBps,
potentialAffiliateBps,
rate,
slippageTolerancePercentageDecimal: (slippageTolerancePercentage / 100).toString(),
steps: [
{
accountNumber,
allowanceContract,
rate,
buyAsset,
sellAsset,
buyAmountBeforeFeesCryptoBaseUnit,
buyAmountAfterFeesCryptoBaseUnit,
sellAmountIncludingProtocolFeesCryptoBaseUnit:
input.sellAmountIncludingProtocolFeesCryptoBaseUnit,
feeData: {
networkFeeCryptoBaseUnit,
// Protocol fees are always denominated in buy asset here, this is the downside on the swap
protocolFees: {
[buyAsset.assetId]: {
amountCryptoBaseUnit: feeAmount,
asset: buyAsset,
requiresBalance: false,
},
},
},
source: SwapperName.Portals,
estimatedExecutionTimeMs: undefined, // Portals doesn't provide this info
},
] as SingleHopTradeQuoteSteps,
}

return Ok(tradeQuote)
} catch (err) {
return Err(
makeSwapErrorRight({
message: 'failed to get Portals quote',
cause: err,
code: TradeQuoteError.NetworkFeeEstimationFailed,
}),
)
}
}
16 changes: 16 additions & 0 deletions packages/swapper/src/swappers/PortalsSwapper/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { KnownChainIds } from '@shapeshiftoss/types'

// https://api.portals.fi/v1/networks
export const PortalsSupportedChainIds = [
KnownChainIds.EthereumMainnet,
KnownChainIds.ArbitrumMainnet,
KnownChainIds.AvalancheMainnet,
KnownChainIds.PolygonMainnet,
KnownChainIds.BnbSmartChainMainnet,
KnownChainIds.OptimismMainnet,
KnownChainIds.ArbitrumMainnet,
KnownChainIds.GnosisMainnet,
KnownChainIds.BaseMainnet,
] as const

export type PortalsSupportedChainId = (typeof PortalsSupportedChainIds)[number]
Loading
Loading