diff --git a/apps/defi/src/lang/en.json b/apps/defi/src/lang/en.json index 1e40f286a..534f8075a 100644 --- a/apps/defi/src/lang/en.json +++ b/apps/defi/src/lang/en.json @@ -2503,6 +2503,12 @@ "value": "Error while estimating" } ], + "iQgt9N": [ + { + "type": 0, + "value": "Swap via Balancer" + } + ], "iaS0YY": [ { "type": 0, diff --git a/apps/defi/src/lang/extracts/en.json b/apps/defi/src/lang/extracts/en.json index f93f889fa..f8524eede 100644 --- a/apps/defi/src/lang/extracts/en.json +++ b/apps/defi/src/lang/extracts/en.json @@ -983,6 +983,9 @@ "iLgjES": { "defaultMessage": "Error while estimating" }, + "iQgt9N": { + "defaultMessage": "Swap via Balancer" + }, "iaS0YY": { "defaultMessage": "I have read and agree to the above terms" }, diff --git a/libs/defi/oeth/src/swap/actions.ts b/libs/defi/oeth/src/swap/actions.ts index e1e5a4634..589a3350c 100644 --- a/libs/defi/oeth/src/swap/actions.ts +++ b/libs/defi/oeth/src/swap/actions.ts @@ -1,5 +1,6 @@ import { mintVaultOeth, + swapBalancerOeth, SwapCurveOeth, swapCurveOethEth, swapCurveOethSfrxeth, @@ -15,6 +16,11 @@ import type { SwapApi } from '@origin/shared/providers'; import type { OethSwapAction } from './types'; export const oethSwapActions: Record = { + 'swap-balancer-oeth': { + ...swapBalancerOeth, + routeLabel: defineMessage({ defaultMessage: 'Swap via Balancer' }), + buttonLabel: defineMessage({ defaultMessage: 'Swap' }), + }, 'swap-curve-oeth': { ...SwapCurveOeth, routeLabel: defineMessage({ defaultMessage: 'Swap via Curve' }), diff --git a/libs/defi/oeth/src/swap/constants.ts b/libs/defi/oeth/src/swap/constants.ts index bf8c5c087..407dd38b1 100644 --- a/libs/defi/oeth/src/swap/constants.ts +++ b/libs/defi/oeth/src/swap/constants.ts @@ -6,6 +6,16 @@ import type { OethSwapAction } from './types'; export const oethSwapRoutes: SwapRoute[] = [ // Mint + { + tokenIn: tokens.arbitrum.ETH, + tokenOut: tokens.arbitrum.wOETH, + action: 'swap-balancer-oeth', + }, + { + tokenIn: tokens.arbitrum.WETH, + tokenOut: tokens.arbitrum.wOETH, + action: 'swap-balancer-oeth', + }, { tokenIn: tokens.mainnet.ETH, tokenOut: tokens.mainnet.OETH, @@ -29,6 +39,16 @@ export const oethSwapRoutes: SwapRoute[] = [ action: 'swap-curve-oeth', }, // Redeem + { + tokenIn: tokens.arbitrum.wOETH, + tokenOut: tokens.arbitrum.ETH, + action: 'swap-balancer-oeth', + }, + { + tokenIn: tokens.arbitrum.wOETH, + tokenOut: tokens.arbitrum.WETH, + action: 'swap-balancer-oeth', + }, { tokenIn: tokens.mainnet.OETH, tokenOut: tokens.mainnet.WETH, diff --git a/libs/defi/oeth/src/swap/types.ts b/libs/defi/oeth/src/swap/types.ts index a4f9e4027..a510eab46 100644 --- a/libs/defi/oeth/src/swap/types.ts +++ b/libs/defi/oeth/src/swap/types.ts @@ -3,6 +3,7 @@ import type { OethRoute } from '@origin/shared/routes'; export type OethSwapAction = Extract< OethRoute, | 'mint-vault-oeth' + | 'swap-balancer-oeth' | 'swap-curve-oeth' | 'swap-curve-oeth-eth' | 'swap-curve-oeth-sfrxeth' diff --git a/libs/defi/oeth/src/swap/views/SwapView.tsx b/libs/defi/oeth/src/swap/views/SwapView.tsx index bf3acd4aa..9e0e084f5 100644 --- a/libs/defi/oeth/src/swap/views/SwapView.tsx +++ b/libs/defi/oeth/src/swap/views/SwapView.tsx @@ -11,6 +11,8 @@ import { } from '@origin/defi/shared'; import { tokens } from '@origin/shared/contracts'; import { useIntl } from 'react-intl'; +import { mainnet } from 'viem/chains'; +import { useAccount } from 'wagmi'; import { oethSwapActions } from '../actions'; import { AnalyticsCard } from '../components/AnalyticsCard'; @@ -19,6 +21,13 @@ import { oethSwapRoutes } from '../constants'; export const SwapView = () => { const intl = useIntl(); + const { chain } = useAccount(); + + const token = + (chain ?? mainnet).id === mainnet.id + ? tokens.mainnet.OETH + : tokens.arbitrum.wOETH; + return ( { - + diff --git a/libs/defi/shared/src/components/Cards/GlobalStatsCard.tsx b/libs/defi/shared/src/components/Cards/GlobalStatsCard.tsx index 9164affdb..623c2fe4a 100644 --- a/libs/defi/shared/src/components/Cards/GlobalStatsCard.tsx +++ b/libs/defi/shared/src/components/Cards/GlobalStatsCard.tsx @@ -1,5 +1,6 @@ import { Card, CardContent, CardHeader, Divider, Stack } from '@mui/material'; import { ValueLabel } from '@origin/shared/components'; +import { supportedChainNames } from '@origin/shared/constants'; import { getTokenPriceKey, useTokenPrice, @@ -35,9 +36,11 @@ export const GlobalStatsCard = ({ token, ...rest }: GlobalStatsCardProps) => { labelProps={{ variant: 'body3', fontWeight: 'medium' }} labelInfoTooltip={intl.formatMessage( { - defaultMessage: 'Total value locked {symbol}', + defaultMessage: 'Total value locked on {chainName}', + }, + { + chainName: supportedChainNames[token.chainId].short, }, - { symbol: token.symbol }, )} value={`$${format(tvl ?? from(0), 2)}`} valueProps={{ fontWeight: 'medium' }} @@ -47,6 +50,15 @@ export const GlobalStatsCard = ({ token, ...rest }: GlobalStatsCardProps) => { direction="row" justifyContent="space-between" label={intl.formatMessage({ defaultMessage: 'Price' })} + labelInfoTooltip={intl.formatMessage( + { + defaultMessage: 'USD price of {symbol} on {chainName}', + }, + { + symbol: token.symbol, + chainName: supportedChainNames[token.chainId].short, + }, + )} labelProps={{ variant: 'body3', fontWeight: 'medium' }} value={`$${format(price ?? from(0), 2)}`} valueProps={{ fontWeight: 'medium' }} diff --git a/libs/shared/constants/src/chains.ts b/libs/shared/constants/src/chains.ts index fefb47e18..1488053da 100644 --- a/libs/shared/constants/src/chains.ts +++ b/libs/shared/constants/src/chains.ts @@ -1,5 +1,11 @@ import { arbitrum, mainnet, optimism } from 'viem/chains'; +export const supportedChains = { + [mainnet.id.toString()]: mainnet, + [arbitrum.id.toString()]: arbitrum, + [optimism.id.toString()]: optimism, +} as const; + export const supportedChainNames = { [mainnet.id.toString()]: { short: 'Ethereum', diff --git a/libs/shared/contracts/src/abis/BalancerQueries.ts b/libs/shared/contracts/src/abis/BalancerQueries.ts new file mode 100644 index 000000000..8c81df90e --- /dev/null +++ b/libs/shared/contracts/src/abis/BalancerQueries.ts @@ -0,0 +1,161 @@ +export const BalancerQueriesABI = [ + { + inputs: [ + { internalType: 'contract IVault', name: '_vault', type: 'address' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + inputs: [ + { internalType: 'enum IVault.SwapKind', name: 'kind', type: 'uint8' }, + { + components: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'uint256', name: 'assetInIndex', type: 'uint256' }, + { internalType: 'uint256', name: 'assetOutIndex', type: 'uint256' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IVault.BatchSwapStep[]', + name: 'swaps', + type: 'tuple[]', + }, + { internalType: 'contract IAsset[]', name: 'assets', type: 'address[]' }, + { + components: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'bool', name: 'fromInternalBalance', type: 'bool' }, + { + internalType: 'address payable', + name: 'recipient', + type: 'address', + }, + { internalType: 'bool', name: 'toInternalBalance', type: 'bool' }, + ], + internalType: 'struct IVault.FundManagement', + name: 'funds', + type: 'tuple', + }, + ], + name: 'queryBatchSwap', + outputs: [ + { internalType: 'int256[]', name: 'assetDeltas', type: 'int256[]' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { + components: [ + { + internalType: 'contract IAsset[]', + name: 'assets', + type: 'address[]', + }, + { + internalType: 'uint256[]', + name: 'minAmountsOut', + type: 'uint256[]', + }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + { internalType: 'bool', name: 'toInternalBalance', type: 'bool' }, + ], + internalType: 'struct IVault.ExitPoolRequest', + name: 'request', + type: 'tuple', + }, + ], + name: 'queryExit', + outputs: [ + { internalType: 'uint256', name: 'bptIn', type: 'uint256' }, + { internalType: 'uint256[]', name: 'amountsOut', type: 'uint256[]' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { + components: [ + { + internalType: 'contract IAsset[]', + name: 'assets', + type: 'address[]', + }, + { + internalType: 'uint256[]', + name: 'maxAmountsIn', + type: 'uint256[]', + }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + { internalType: 'bool', name: 'fromInternalBalance', type: 'bool' }, + ], + internalType: 'struct IVault.JoinPoolRequest', + name: 'request', + type: 'tuple', + }, + ], + name: 'queryJoin', + outputs: [ + { internalType: 'uint256', name: 'bptOut', type: 'uint256' }, + { internalType: 'uint256[]', name: 'amountsIn', type: 'uint256[]' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'enum IVault.SwapKind', name: 'kind', type: 'uint8' }, + { internalType: 'contract IAsset', name: 'assetIn', type: 'address' }, + { + internalType: 'contract IAsset', + name: 'assetOut', + type: 'address', + }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IVault.SingleSwap', + name: 'singleSwap', + type: 'tuple', + }, + { + components: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'bool', name: 'fromInternalBalance', type: 'bool' }, + { + internalType: 'address payable', + name: 'recipient', + type: 'address', + }, + { internalType: 'bool', name: 'toInternalBalance', type: 'bool' }, + ], + internalType: 'struct IVault.FundManagement', + name: 'funds', + type: 'tuple', + }, + ], + name: 'querySwap', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'vault', + outputs: [{ internalType: 'contract IVault', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, +] as const; diff --git a/libs/shared/contracts/src/abis/BalancerVault.ts b/libs/shared/contracts/src/abis/BalancerVault.ts new file mode 100644 index 000000000..442ced51e --- /dev/null +++ b/libs/shared/contracts/src/abis/BalancerVault.ts @@ -0,0 +1,756 @@ +export const BalancerVaultABI = [ + { + inputs: [ + { + internalType: 'contract IAuthorizer', + name: 'authorizer', + type: 'address', + }, + { internalType: 'contract IWETH', name: 'weth', type: 'address' }, + { internalType: 'uint256', name: 'pauseWindowDuration', type: 'uint256' }, + { + internalType: 'uint256', + name: 'bufferPeriodDuration', + type: 'uint256', + }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'contract IAuthorizer', + name: 'newAuthorizer', + type: 'address', + }, + ], + name: 'AuthorizerChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'contract IERC20', + name: 'token', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: 'recipient', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'ExternalBalanceTransfer', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'contract IFlashLoanRecipient', + name: 'recipient', + type: 'address', + }, + { + indexed: true, + internalType: 'contract IERC20', + name: 'token', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'feeAmount', + type: 'uint256', + }, + ], + name: 'FlashLoan', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + { + indexed: true, + internalType: 'contract IERC20', + name: 'token', + type: 'address', + }, + { indexed: false, internalType: 'int256', name: 'delta', type: 'int256' }, + ], + name: 'InternalBalanceChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'bool', name: 'paused', type: 'bool' }, + ], + name: 'PausedStateChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'poolId', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'address', + name: 'liquidityProvider', + type: 'address', + }, + { + indexed: false, + internalType: 'contract IERC20[]', + name: 'tokens', + type: 'address[]', + }, + { + indexed: false, + internalType: 'int256[]', + name: 'deltas', + type: 'int256[]', + }, + { + indexed: false, + internalType: 'uint256[]', + name: 'protocolFeeAmounts', + type: 'uint256[]', + }, + ], + name: 'PoolBalanceChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'poolId', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'address', + name: 'assetManager', + type: 'address', + }, + { + indexed: true, + internalType: 'contract IERC20', + name: 'token', + type: 'address', + }, + { + indexed: false, + internalType: 'int256', + name: 'cashDelta', + type: 'int256', + }, + { + indexed: false, + internalType: 'int256', + name: 'managedDelta', + type: 'int256', + }, + ], + name: 'PoolBalanceManaged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'poolId', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'address', + name: 'poolAddress', + type: 'address', + }, + { + indexed: false, + internalType: 'enum IVault.PoolSpecialization', + name: 'specialization', + type: 'uint8', + }, + ], + name: 'PoolRegistered', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'relayer', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + { indexed: false, internalType: 'bool', name: 'approved', type: 'bool' }, + ], + name: 'RelayerApprovalChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'poolId', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'contract IERC20', + name: 'tokenIn', + type: 'address', + }, + { + indexed: true, + internalType: 'contract IERC20', + name: 'tokenOut', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amountIn', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amountOut', + type: 'uint256', + }, + ], + name: 'Swap', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'poolId', + type: 'bytes32', + }, + { + indexed: false, + internalType: 'contract IERC20[]', + name: 'tokens', + type: 'address[]', + }, + ], + name: 'TokensDeregistered', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'poolId', + type: 'bytes32', + }, + { + indexed: false, + internalType: 'contract IERC20[]', + name: 'tokens', + type: 'address[]', + }, + { + indexed: false, + internalType: 'address[]', + name: 'assetManagers', + type: 'address[]', + }, + ], + name: 'TokensRegistered', + type: 'event', + }, + { + inputs: [], + name: 'WETH', + outputs: [{ internalType: 'contract IWETH', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'enum IVault.SwapKind', name: 'kind', type: 'uint8' }, + { + components: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'uint256', name: 'assetInIndex', type: 'uint256' }, + { internalType: 'uint256', name: 'assetOutIndex', type: 'uint256' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IVault.BatchSwapStep[]', + name: 'swaps', + type: 'tuple[]', + }, + { internalType: 'contract IAsset[]', name: 'assets', type: 'address[]' }, + { + components: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'bool', name: 'fromInternalBalance', type: 'bool' }, + { + internalType: 'address payable', + name: 'recipient', + type: 'address', + }, + { internalType: 'bool', name: 'toInternalBalance', type: 'bool' }, + ], + internalType: 'struct IVault.FundManagement', + name: 'funds', + type: 'tuple', + }, + { internalType: 'int256[]', name: 'limits', type: 'int256[]' }, + { internalType: 'uint256', name: 'deadline', type: 'uint256' }, + ], + name: 'batchSwap', + outputs: [ + { internalType: 'int256[]', name: 'assetDeltas', type: 'int256[]' }, + ], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'contract IERC20[]', name: 'tokens', type: 'address[]' }, + ], + name: 'deregisterTokens', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + { + components: [ + { + internalType: 'contract IAsset[]', + name: 'assets', + type: 'address[]', + }, + { + internalType: 'uint256[]', + name: 'minAmountsOut', + type: 'uint256[]', + }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + { internalType: 'bool', name: 'toInternalBalance', type: 'bool' }, + ], + internalType: 'struct IVault.ExitPoolRequest', + name: 'request', + type: 'tuple', + }, + ], + name: 'exitPool', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'contract IFlashLoanRecipient', + name: 'recipient', + type: 'address', + }, + { internalType: 'contract IERC20[]', name: 'tokens', type: 'address[]' }, + { internalType: 'uint256[]', name: 'amounts', type: 'uint256[]' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'flashLoan', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes4', name: 'selector', type: 'bytes4' }], + name: 'getActionId', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getAuthorizer', + outputs: [ + { internalType: 'contract IAuthorizer', name: '', type: 'address' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getDomainSeparator', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'user', type: 'address' }, + { internalType: 'contract IERC20[]', name: 'tokens', type: 'address[]' }, + ], + name: 'getInternalBalance', + outputs: [ + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'user', type: 'address' }], + name: 'getNextNonce', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getPausedState', + outputs: [ + { internalType: 'bool', name: 'paused', type: 'bool' }, + { internalType: 'uint256', name: 'pauseWindowEndTime', type: 'uint256' }, + { internalType: 'uint256', name: 'bufferPeriodEndTime', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: 'poolId', type: 'bytes32' }], + name: 'getPool', + outputs: [ + { internalType: 'address', name: '', type: 'address' }, + { + internalType: 'enum IVault.PoolSpecialization', + name: '', + type: 'uint8', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'contract IERC20', name: 'token', type: 'address' }, + ], + name: 'getPoolTokenInfo', + outputs: [ + { internalType: 'uint256', name: 'cash', type: 'uint256' }, + { internalType: 'uint256', name: 'managed', type: 'uint256' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'address', name: 'assetManager', type: 'address' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: 'poolId', type: 'bytes32' }], + name: 'getPoolTokens', + outputs: [ + { internalType: 'contract IERC20[]', name: 'tokens', type: 'address[]' }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getProtocolFeesCollector', + outputs: [ + { + internalType: 'contract ProtocolFeesCollector', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'user', type: 'address' }, + { internalType: 'address', name: 'relayer', type: 'address' }, + ], + name: 'hasApprovedRelayer', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { + components: [ + { + internalType: 'contract IAsset[]', + name: 'assets', + type: 'address[]', + }, + { + internalType: 'uint256[]', + name: 'maxAmountsIn', + type: 'uint256[]', + }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + { internalType: 'bool', name: 'fromInternalBalance', type: 'bool' }, + ], + internalType: 'struct IVault.JoinPoolRequest', + name: 'request', + type: 'tuple', + }, + ], + name: 'joinPool', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'enum IVault.PoolBalanceOpKind', + name: 'kind', + type: 'uint8', + }, + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'contract IERC20', name: 'token', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + internalType: 'struct IVault.PoolBalanceOp[]', + name: 'ops', + type: 'tuple[]', + }, + ], + name: 'managePoolBalance', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'enum IVault.UserBalanceOpKind', + name: 'kind', + type: 'uint8', + }, + { internalType: 'contract IAsset', name: 'asset', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { + internalType: 'address payable', + name: 'recipient', + type: 'address', + }, + ], + internalType: 'struct IVault.UserBalanceOp[]', + name: 'ops', + type: 'tuple[]', + }, + ], + name: 'manageUserBalance', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { internalType: 'enum IVault.SwapKind', name: 'kind', type: 'uint8' }, + { + components: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'uint256', name: 'assetInIndex', type: 'uint256' }, + { internalType: 'uint256', name: 'assetOutIndex', type: 'uint256' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IVault.BatchSwapStep[]', + name: 'swaps', + type: 'tuple[]', + }, + { internalType: 'contract IAsset[]', name: 'assets', type: 'address[]' }, + { + components: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'bool', name: 'fromInternalBalance', type: 'bool' }, + { + internalType: 'address payable', + name: 'recipient', + type: 'address', + }, + { internalType: 'bool', name: 'toInternalBalance', type: 'bool' }, + ], + internalType: 'struct IVault.FundManagement', + name: 'funds', + type: 'tuple', + }, + ], + name: 'queryBatchSwap', + outputs: [{ internalType: 'int256[]', name: '', type: 'int256[]' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'enum IVault.PoolSpecialization', + name: 'specialization', + type: 'uint8', + }, + ], + name: 'registerPool', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'contract IERC20[]', name: 'tokens', type: 'address[]' }, + { internalType: 'address[]', name: 'assetManagers', type: 'address[]' }, + ], + name: 'registerTokens', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'contract IAuthorizer', + name: 'newAuthorizer', + type: 'address', + }, + ], + name: 'setAuthorizer', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bool', name: 'paused', type: 'bool' }], + name: 'setPaused', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'relayer', type: 'address' }, + { internalType: 'bool', name: 'approved', type: 'bool' }, + ], + name: 'setRelayerApproval', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'enum IVault.SwapKind', name: 'kind', type: 'uint8' }, + { internalType: 'contract IAsset', name: 'assetIn', type: 'address' }, + { + internalType: 'contract IAsset', + name: 'assetOut', + type: 'address', + }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IVault.SingleSwap', + name: 'singleSwap', + type: 'tuple', + }, + { + components: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'bool', name: 'fromInternalBalance', type: 'bool' }, + { + internalType: 'address payable', + name: 'recipient', + type: 'address', + }, + { internalType: 'bool', name: 'toInternalBalance', type: 'bool' }, + ], + internalType: 'struct IVault.FundManagement', + name: 'funds', + type: 'tuple', + }, + { internalType: 'uint256', name: 'limit', type: 'uint256' }, + { internalType: 'uint256', name: 'deadline', type: 'uint256' }, + ], + name: 'swap', + outputs: [ + { internalType: 'uint256', name: 'amountCalculated', type: 'uint256' }, + ], + stateMutability: 'payable', + type: 'function', + }, + { stateMutability: 'payable', type: 'receive' }, +] as const; diff --git a/libs/shared/contracts/src/contracts.ts b/libs/shared/contracts/src/contracts.ts index 2f9831646..33385a08a 100644 --- a/libs/shared/contracts/src/contracts.ts +++ b/libs/shared/contracts/src/contracts.ts @@ -1,5 +1,7 @@ import { arbitrum, mainnet } from 'wagmi/chains'; +import { BalancerQueriesABI } from './abis/BalancerQueries'; +import { BalancerVaultABI } from './abis/BalancerVault'; import { CCIPEvm2EvmOnRamp } from './abis/CCIPEvm2EvmOnRamp'; import { CCIPRouterABI } from './abis/CCIPRouter'; import { ChainlinkOracleABI } from './abis/ChainlinkOracle'; @@ -239,6 +241,19 @@ export const contracts = { }, }, arbitrum: { + // Balancer + balancerQueries: { + address: '0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5', + chainId: arbitrum.id, + abi: BalancerQueriesABI, + name: 'balancerQueries', + }, + balancerVault: { + address: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', + chainId: arbitrum.id, + abi: BalancerVaultABI, + name: 'balancerVault', + }, // Chainlink CCIP ccipRouter: { address: '0x141fa059441E0ca23ce184B6A78bafD2A517DdE8', diff --git a/libs/shared/contracts/src/types.ts b/libs/shared/contracts/src/types.ts index 640f52fd4..b9d0a78fc 100644 --- a/libs/shared/contracts/src/types.ts +++ b/libs/shared/contracts/src/types.ts @@ -1,8 +1,11 @@ +import type { supportedChains } from '@origin/shared/constants'; import type { HexAddress } from '@origin/shared/utils'; import type { Abi } from 'viem'; import type { tokenList } from './tokens'; +export type SupportedChainId = keyof typeof supportedChains; + export type Contract = { address: HexAddress; chainId: number; diff --git a/libs/shared/contracts/src/utils.ts b/libs/shared/contracts/src/utils.ts index 34a596450..b880b918f 100644 --- a/libs/shared/contracts/src/utils.ts +++ b/libs/shared/contracts/src/utils.ts @@ -1,8 +1,10 @@ +import { supportedChains } from '@origin/shared/constants'; + import { tokenIdMap, tokenList } from './tokens'; import type { Chain } from 'viem/chains'; -import type { Token, TokenId } from './types'; +import type { SupportedChainId, Token, TokenId } from './types'; export const getNativeToken = (chain: Chain): Token => { return { @@ -14,6 +16,12 @@ export const getNativeToken = (chain: Chain): Token => { }; }; +export const getNativeTokenByChainId = (chainId: SupportedChainId): Token => { + const chain = supportedChains[chainId]; + + return getNativeToken(chain); +}; + export const getTokenByAddress = ( address: string | undefined, chainId?: number, diff --git a/libs/shared/contracts/src/whales.ts b/libs/shared/contracts/src/whales.ts index e39df71e2..f823de850 100644 --- a/libs/shared/contracts/src/whales.ts +++ b/libs/shared/contracts/src/whales.ts @@ -8,4 +8,9 @@ export const whales = { OUSD: '0x70fCE97d671E81080CA3ab4cc7A59aAc2E117137', wOUSD: '0x3dD413Fd4D03b1d8fD2C9Ed34553F7DeC3B26F5C', }, + arbitrum: { + ETH: '0xF977814e90dA44bFA03b6295A0616a897441aceC', + wOETH: '0x9f4D6f98F29c1D482bCF0F85683155E0B3e015f5', + WETH: '0x70d95587d40A2caf56bd97485aB3Eec10Bee6336', + }, } as const; diff --git a/libs/shared/providers/src/gas/hooks.ts b/libs/shared/providers/src/gas/hooks.ts index aad411091..faa8295b2 100644 --- a/libs/shared/providers/src/gas/hooks.ts +++ b/libs/shared/providers/src/gas/hooks.ts @@ -1,10 +1,11 @@ +import { getNativeTokenByChainId } from '@origin/shared/contracts'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { estimateFeesPerGas } from '@wagmi/core'; import { add, from, mul } from 'dnum'; import { useConfig } from 'wagmi'; import { mainnet } from 'wagmi/chains'; -import { useTokenPrice } from '../prices'; +import { getTokenPriceKey, useTokenPrice } from '../prices'; import type { QueryClient, @@ -38,7 +39,9 @@ const fetcher: ( async ({ queryKey: [, gasAmount, chainId] }) => { const [price, data] = await Promise.all([ queryClient.fetchQuery({ - queryKey: useTokenPrice.getKey('1:ETH_USD'), + queryKey: useTokenPrice.getKey( + getTokenPriceKey(getNativeTokenByChainId(chainId)), + ), queryFn: useTokenPrice.fetcher(config, queryClient), }), estimateFeesPerGas(config, { chainId }), diff --git a/libs/shared/providers/src/prices/constants.ts b/libs/shared/providers/src/prices/constants.ts index cbd164b8b..9b3abc532 100644 --- a/libs/shared/providers/src/prices/constants.ts +++ b/libs/shared/providers/src/prices/constants.ts @@ -8,7 +8,7 @@ import { import { from } from 'dnum'; import { pathOr } from 'ramda'; import { parseUnits } from 'viem'; -import { mainnet } from 'wagmi/chains'; +import { arbitrum, mainnet } from 'wagmi/chains'; import type { Dnum } from 'dnum'; @@ -47,6 +47,7 @@ export const chainlinkOraclesMainnet = { export const chainlinkOraclesArbitrum = { ETH_USD: '0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612', + wOETH_OETH: '0x03a1f4b19aaeA6e68f0f104dc4346dA3E942cC45', } as const; const chainLinkUsdMapper = (data: any) => [pathOr(0n, [1], data), 8] as Dnum; @@ -338,4 +339,36 @@ export const priceOptions: Partial> = { type: 'rest', config: async () => from(1 - OETH_REDEEM_FEE), }, + '42161:ETH_USD': { + id: '42161:ETH_USD', + type: 'wagmi', + config: { + address: chainlinkOraclesArbitrum.ETH_USD, + abi: ChainlinkAggregatorABI, + functionName: 'latestRoundData', + chainId: arbitrum.id, + }, + mapResult: chainLinkUsdMapper, + }, + '42161:wOETH_42161:OETH': { + id: '42161:wOETH_42161:OETH', + type: 'wagmi', + config: { + address: chainlinkOraclesArbitrum.wOETH_OETH, + abi: ChainlinkAggregatorABI, + functionName: 'latestRoundData', + chainId: arbitrum.id, + }, + mapResult: chainLinkEthMapper, + }, + '42161:wOETH_USD': { + id: '42161:wOETH_USD', + type: 'derived', + dependsOn: ['42161:wOETH_42161:OETH', '42161:ETH_USD'], + }, + '42161:WETH_USD': { + id: '42161:WETH_USD', + type: 'derived', + dependsOn: ['42161:ETH_USD'], + }, }; diff --git a/libs/shared/providers/src/prices/types.ts b/libs/shared/providers/src/prices/types.ts index 6647ec0f9..0ec9bc409 100644 --- a/libs/shared/providers/src/prices/types.ts +++ b/libs/shared/providers/src/prices/types.ts @@ -60,10 +60,18 @@ export type SupportedToken = Extract< | '1:WETH' | '1:wOETH' | '1:wOUSD' + | '42161:ETH' + | '42161:WETH' + | '42161:OETH' + | '42161:wOETH' >; export type SupportedCurrency = | 'USD' - | Extract; + | '42161:OETH' + | Extract< + TokenId, + '1:ETH' | '1:frxETH' | '1:OETH' | '1:OUSD' | '1:primeETH' | '42161:ETH' + >; export type SupportedTokenPrice = `${SupportedToken}_${SupportedCurrency}`; diff --git a/libs/shared/providers/src/swapper/state.ts b/libs/shared/providers/src/swapper/state.ts index 46d371598..e8983d6cc 100644 --- a/libs/shared/providers/src/swapper/state.ts +++ b/libs/shared/providers/src/swapper/state.ts @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { formatError, @@ -9,10 +9,11 @@ import { import { useDebouncedEffect } from '@react-hookz/web'; import { useQueryClient } from '@tanstack/react-query'; import { createContainer } from 'react-tracked'; -import { useConfig } from 'wagmi'; +import { mainnet } from 'viem/chains'; +import { useAccount, useConfig } from 'wagmi'; import { useSlippage } from '../slippage'; -import { getAvailableRoutes } from './utils'; +import { getAvailableRoutes, getFilteredSwapRoutes } from './utils'; import type { Dispatch, SetStateAction } from 'react'; @@ -65,13 +66,25 @@ export const { Provider: SwapProvider, useTracked: useSwapState } = onSwapReject, onSwapFailure, }) => { + const { chain } = useAccount(); + const queryClient = useQueryClient(); + const config = useConfig(); + const { value: slippage } = useSlippage(); + const filteredSwapRoutes = getFilteredSwapRoutes( + swapRoutes, + chain ?? mainnet, + ); + const routes = isNilOrEmpty(filteredSwapRoutes) + ? swapRoutes + : filteredSwapRoutes; + const [state, setState] = useState({ swapActions, - swapRoutes: swapRoutes, + swapRoutes: routes, amountIn: 0n, - tokenIn: swapRoutes[0]?.tokenIn, + tokenIn: routes[0]?.tokenIn, amountOut: 0n, - tokenOut: swapRoutes[0]?.tokenOut, + tokenOut: routes[0]?.tokenOut, estimatedSwapRoutes: [], selectedSwapRoute: null, isSwapWaitingForSignature: false, @@ -98,9 +111,25 @@ export const { Provider: SwapProvider, useTracked: useSwapState } = onSwapReject, onSwapFailure, }); - const queryClient = useQueryClient(); - const config = useConfig(); - const { value: slippage } = useSlippage(); + + useEffect(() => { + const filteredSwapRoutes = getFilteredSwapRoutes( + swapRoutes, + chain ?? mainnet, + ); + const routes = isNilOrEmpty(filteredSwapRoutes) + ? swapRoutes + : filteredSwapRoutes; + setState((prev) => ({ + ...prev, + swapRoutes: routes, + tokenIn: routes[0]?.tokenIn, + amountOut: 0n, + tokenOut: routes[0]?.tokenOut, + estimatedSwapRoutes: [], + selectedSwapRoute: null, + })); + }, [chain, swapRoutes]); useDebouncedEffect( async () => { diff --git a/libs/shared/providers/src/swapper/utils.ts b/libs/shared/providers/src/swapper/utils.ts index ff5782a4c..7c0c4ce89 100644 --- a/libs/shared/providers/src/swapper/utils.ts +++ b/libs/shared/providers/src/swapper/utils.ts @@ -115,8 +115,8 @@ export const routeEq = ( } return ( - a.tokenIn.symbol === b.tokenIn.symbol && - a.tokenOut.symbol === b.tokenOut.symbol && + a.tokenIn.id === b.tokenIn.id && + a.tokenOut.id === b.tokenOut.id && a.action === b.action ); }; diff --git a/libs/shared/routes/src/oeth/index.ts b/libs/shared/routes/src/oeth/index.ts index 217a220cb..f5a20d8c2 100644 --- a/libs/shared/routes/src/oeth/index.ts +++ b/libs/shared/routes/src/oeth/index.ts @@ -1,5 +1,6 @@ export * from './mintVaultOeth'; export * from './redeemVaultOeth'; +export * from './swapBalancerOeth'; export * from './swapCurveOeth'; export * from './swapCurveOethEth'; export * from './swapCurveOethSfrxeth'; diff --git a/libs/shared/routes/src/oeth/swapBalancerOeth.ts b/libs/shared/routes/src/oeth/swapBalancerOeth.ts new file mode 100644 index 000000000..bda1e4f21 --- /dev/null +++ b/libs/shared/routes/src/oeth/swapBalancerOeth.ts @@ -0,0 +1,298 @@ +import { contracts, whales } from '@origin/shared/contracts'; +import { + isNilOrEmpty, + subPercentage, + ZERO_ADDRESS, +} from '@origin/shared/utils'; +import { + getAccount, + getPublicClient, + readContract, + simulateContract, + writeContract, +} from '@wagmi/core'; +import { erc20Abi, formatUnits, maxUint256 } from 'viem'; + +import { defaultRoute } from '../defaultRoute'; + +import type { + Allowance, + Approve, + EstimateApprovalGas, + EstimateGas, + EstimateRoute, + IsRouteAvailable, + Swap, +} from '@origin/shared/providers'; +import type { EstimateAmount } from '@origin/shared/providers'; + +const WethWoethPoolId = + '0xef0c116a2818a5b1a5d836a291856a321f43c2fb00020000000000000000053a'; +const defaultUserData = '0x'; +const deadline = 999999999999999999n; + +const isRouteAvailable: IsRouteAvailable = async ({ config }, { tokenIn }) => { + try { + const pausedState = await readContract(config, { + address: contracts.arbitrum.balancerVault.address, + abi: contracts.arbitrum.balancerVault.abi, + functionName: 'getPausedState', + chainId: contracts.arbitrum.balancerVault.chainId, + }); + + return !pausedState?.[0]; + } catch {} + + return false; +}; + +const estimateAmount: EstimateAmount = async ( + { config }, + { tokenIn, tokenOut, amountIn }, +) => { + if (amountIn === 0n) { + return 0n; + } + + const quote = await readContract(config, { + address: contracts.arbitrum.balancerQueries.address, + abi: contracts.arbitrum.balancerQueries.abi, + functionName: 'querySwap', + args: [ + { + poolId: WethWoethPoolId, + kind: 0, + assetIn: tokenIn.address ?? ZERO_ADDRESS, + assetOut: tokenOut.address ?? ZERO_ADDRESS, + amount: amountIn, + userData: defaultUserData, + }, + { + sender: ZERO_ADDRESS, + fromInternalBalance: false, + recipient: ZERO_ADDRESS, + toInternalBalance: false, + }, + ], + }); + + return quote; +}; + +const estimateGas: EstimateGas = async ( + { config }, + { tokenIn, tokenOut, amountIn, slippage, amountOut }, +) => { + let gasEstimate = 0n; + const publicClient = getPublicClient(config, { chainId: tokenIn.chainId }); + + if (amountIn === 0n || !publicClient) { + return gasEstimate; + } + + const minAmountOut = subPercentage( + [amountOut ?? 0n, tokenOut.decimals], + slippage, + ); + const user = isNilOrEmpty(tokenIn?.address) + ? whales.arbitrum.ETH + : whales.arbitrum.wOETH; + + try { + gasEstimate = await publicClient.estimateContractGas({ + address: contracts.arbitrum.balancerVault.address, + abi: contracts.arbitrum.balancerVault.abi, + functionName: 'swap', + args: [ + { + poolId: WethWoethPoolId, + kind: 0, + assetIn: tokenIn.address ?? ZERO_ADDRESS, + assetOut: tokenOut.address ?? ZERO_ADDRESS, + amount: amountIn, + userData: defaultUserData, + }, + { + recipient: user, + sender: user, + fromInternalBalance: false, + toInternalBalance: false, + }, + minAmountOut[0], + deadline, + ], + ...(isNilOrEmpty(tokenIn.address) && { value: amountIn }), + }); + } catch { + gasEstimate = 220_000n; + } + + return gasEstimate; +}; + +const allowance: Allowance = async ({ config }, { tokenIn, tokenOut }) => { + const { address } = getAccount(config); + + if (!address || !tokenIn?.address) { + return maxUint256; + } + + const allowance = await readContract(config, { + address: tokenIn.address, + abi: erc20Abi, + functionName: 'allowance', + args: [address, contracts.arbitrum.balancerVault.address], + chainId: tokenIn.chainId, + }); + + return allowance; +}; + +const estimateApprovalGas: EstimateApprovalGas = async ( + { config }, + { tokenIn, amountIn, tokenOut }, +) => { + let approvalEstimate = 0n; + const { address } = getAccount(config); + const publicClient = getPublicClient(config, { chainId: tokenIn.chainId }); + + if (amountIn === 0n || !address || !publicClient || !tokenIn?.address) { + return approvalEstimate; + } + + try { + approvalEstimate = await publicClient.estimateContractGas({ + address: tokenIn.address, + abi: erc20Abi, + functionName: 'approve', + args: [contracts.arbitrum.balancerVault.address, amountIn], + account: address, + }); + } catch { + approvalEstimate = 200000n; + } + + return approvalEstimate; +}; + +const estimateRoute: EstimateRoute = async ( + config, + { tokenIn, tokenOut, amountIn, route, slippage }, +) => { + if (amountIn === 0n) { + return { + ...route, + estimatedAmount: 0n, + gas: 0n, + rate: 0, + allowanceAmount: 0n, + approvalGas: 0n, + }; + } + + const [estimatedAmount, allowanceAmount, approvalGas] = await Promise.all([ + estimateAmount(config, { tokenIn, tokenOut, amountIn }), + allowance(config, { tokenIn, tokenOut }), + estimateApprovalGas(config, { amountIn, tokenIn, tokenOut }), + ]); + const gas = await estimateGas(config, { + tokenIn, + tokenOut, + amountIn, + slippage, + amountOut: estimatedAmount, + }); + + return { + ...route, + estimatedAmount, + gas, + approvalGas, + allowanceAmount, + rate: + +formatUnits(estimatedAmount, tokenOut.decimals) / + +formatUnits(amountIn, tokenIn.decimals), + }; +}; + +const approve: Approve = async ({ config }, { tokenIn, amountIn }) => { + if (!tokenIn?.address) { + return null; + } + + const { request } = await simulateContract(config, { + address: tokenIn.address, + abi: erc20Abi, + functionName: 'approve', + args: [contracts.arbitrum.balancerVault.address, amountIn], + chainId: tokenIn.chainId, + }); + const hash = await writeContract(config, request); + + return hash; +}; + +const swap: Swap = async ( + { config, queryClient }, + { tokenIn, tokenOut, amountIn, slippage, amountOut }, +) => { + const { address } = getAccount(config); + + if (amountIn === 0n || !address) { + return null; + } + + const approved = await allowance( + { config, queryClient }, + { tokenIn, tokenOut }, + ); + + if (approved < amountIn) { + throw new Error(`Balancer vault is not approved`); + } + + const minAmountOut = subPercentage( + [amountOut ?? 0n, tokenOut.decimals], + slippage, + ); + + const { request } = await simulateContract(config, { + address: contracts.arbitrum.balancerVault.address, + abi: contracts.arbitrum.balancerVault.abi, + functionName: 'swap', + args: [ + { + poolId: WethWoethPoolId, + kind: 0, + assetIn: tokenIn.address ?? ZERO_ADDRESS, + assetOut: tokenOut.address ?? ZERO_ADDRESS, + amount: amountIn, + userData: defaultUserData, + }, + { + recipient: address, + sender: address, + fromInternalBalance: false, + toInternalBalance: false, + }, + minAmountOut[0], + deadline, + ], + ...(isNilOrEmpty(tokenIn.address) && { value: amountIn }), + }); + const hash = await writeContract(config, request); + + return hash; +}; + +export const swapBalancerOeth = { + ...defaultRoute, + isRouteAvailable, + estimateAmount, + estimateGas, + estimateRoute, + allowance, + estimateApprovalGas, + approve, + swap, +}; diff --git a/libs/shared/routes/src/types.ts b/libs/shared/routes/src/types.ts index b63edf9d9..1d25f87bd 100644 --- a/libs/shared/routes/src/types.ts +++ b/libs/shared/routes/src/types.ts @@ -1,6 +1,7 @@ export type OethRoute = | 'mint-vault-oeth' | 'redeem-vault-oeth' + | 'swap-balancer-oeth' | 'swap-curve-oeth' | 'swap-curve-oeth-eth' | 'swap-curve-oeth-sfrxeth'