diff --git a/package-lock.json b/package-lock.json index 4930b9abbf..adc9fee117 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "@balancer/frontend-v2", - "version": "1.110.5", + "version": "1.111.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@balancer/frontend-v2", - "version": "1.110.5", + "version": "1.111.0", "license": "MIT", "devDependencies": { "@aave/protocol-js": "^4.3.0", "@balancer-labs/assets": "github:balancer-labs/assets#master", - "@balancer-labs/sdk": "^1.1.3-beta.0", + "@balancer-labs/sdk": "^1.1.3-beta.2", "@balancer-labs/typechain": "^1.0.0", "@balancer-labs/v2-deployments": "^3.2.0", "@cowprotocol/contracts": "^1.3.1", @@ -38,6 +38,7 @@ "@graphql-codegen/typescript-operations": "^2.2.0", "@graphql-codegen/typescript-resolvers": "2.4.1", "@intlify/unplugin-vue-i18n": "^0.8.1", + "@layerzerolabs/scan-client": "^0.0.5", "@metamask/detect-provider": "^1.2.0", "@popperjs/core": "^2.9.2", "@sentry/browser": "^7.17.4", @@ -1520,9 +1521,9 @@ } }, "node_modules/@balancer-labs/sdk": { - "version": "1.1.3-beta.0", - "resolved": "https://registry.npmjs.org/@balancer-labs/sdk/-/sdk-1.1.3-beta.0.tgz", - "integrity": "sha512-7QZbD1kB75x8Q30YOVhpVDDpwbKrpTdi/kQIZUQW9Z6jLSetsfVDrz+NTEBQguyO2dwLOTSwxfFBaU/2qs9L5Q==", + "version": "1.1.3-beta.2", + "resolved": "https://registry.npmjs.org/@balancer-labs/sdk/-/sdk-1.1.3-beta.2.tgz", + "integrity": "sha512-fnlfvCIdMDbsamfRXDwuwBBOVCk8RYeL2ZtUq2tfPvZAoNcZAkt8axl+Kg9ZFZ7ewoJGVLrbyG88dpoFR16Eow==", "dev": true, "dependencies": { "@balancer-labs/sor": "4.1.1-beta.9", @@ -4776,6 +4777,15 @@ "node": ">= 8.0.0" } }, + "node_modules/@layerzerolabs/scan-client": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@layerzerolabs/scan-client/-/scan-client-0.0.5.tgz", + "integrity": "sha512-qhqYJx0kH4NHxEEOasrZJnoCEIRVs8aFF2Vkn3UG9FplAuj/eEj9jIJyLZNP1F1qz80vy9349Vd0kJcFcetV0Q==", + "dev": true, + "peerDependencies": { + "axios": "*" + } + }, "node_modules/@lit-labs/ssr-dom-shim": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz", @@ -25487,9 +25497,9 @@ } }, "@balancer-labs/sdk": { - "version": "1.1.3-beta.0", - "resolved": "https://registry.npmjs.org/@balancer-labs/sdk/-/sdk-1.1.3-beta.0.tgz", - "integrity": "sha512-7QZbD1kB75x8Q30YOVhpVDDpwbKrpTdi/kQIZUQW9Z6jLSetsfVDrz+NTEBQguyO2dwLOTSwxfFBaU/2qs9L5Q==", + "version": "1.1.3-beta.2", + "resolved": "https://registry.npmjs.org/@balancer-labs/sdk/-/sdk-1.1.3-beta.2.tgz", + "integrity": "sha512-fnlfvCIdMDbsamfRXDwuwBBOVCk8RYeL2ZtUq2tfPvZAoNcZAkt8axl+Kg9ZFZ7ewoJGVLrbyG88dpoFR16Eow==", "dev": true, "requires": { "@balancer-labs/sor": "4.1.1-beta.9", @@ -27921,6 +27931,13 @@ "path-to-regexp": "^6.1.0" } }, + "@layerzerolabs/scan-client": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@layerzerolabs/scan-client/-/scan-client-0.0.5.tgz", + "integrity": "sha512-qhqYJx0kH4NHxEEOasrZJnoCEIRVs8aFF2Vkn3UG9FplAuj/eEj9jIJyLZNP1F1qz80vy9349Vd0kJcFcetV0Q==", + "dev": true, + "requires": {} + }, "@lit-labs/ssr-dom-shim": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz", diff --git a/package.json b/package.json index 4aa3e172a2..373a82eacb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@balancer/frontend-v2", - "version": "1.110.5", + "version": "1.111.0", "engines": { "node": "=16", "npm": ">=8" @@ -42,7 +42,7 @@ "devDependencies": { "@aave/protocol-js": "^4.3.0", "@balancer-labs/assets": "github:balancer-labs/assets#master", - "@balancer-labs/sdk": "^1.1.3-beta.0", + "@balancer-labs/sdk": "^1.1.3-beta.2", "@balancer-labs/typechain": "^1.0.0", "@balancer-labs/v2-deployments": "^3.2.0", "@cowprotocol/contracts": "^1.3.1", @@ -160,7 +160,8 @@ "wait-for-expect": "^3.0.2", "walletlink": "^2.1.5", "web3-utils": "^1.3.1", - "worker-loader": "^3.0.8" + "worker-loader": "^3.0.8", + "@layerzerolabs/scan-client": "^0.0.5" }, "lint-staged": { "*.{js,ts,vue}": "eslint --cache --fix --max-warnings 0", diff --git a/src/App.vue b/src/App.vue index abf4656bd9..c655bdefb4 100644 --- a/src/App.vue +++ b/src/App.vue @@ -41,8 +41,8 @@ const FocussedLayout = defineAsyncComponent( const ContentLayout = defineAsyncComponent( () => import('@/pages/_layouts/ContentLayout.vue') ); -const JoinExitLayout = defineAsyncComponent( - () => import('@/pages/_layouts/JoinExitLayout.vue') +const PoolLayout = defineAsyncComponent( + () => import('@/pages/_layouts/PoolLayout.vue') ); BigNumber.config({ DECIMAL_PLACES: DEFAULT_TOKEN_DECIMALS }); @@ -57,7 +57,7 @@ const Layouts = { ContentLayout: ContentLayout, DefaultLayout: DefaultLayout, FocussedLayout: FocussedLayout, - JoinExitLayout: JoinExitLayout, + PoolLayout: PoolLayout, }; /** * COMPOSABLES diff --git a/src/Root.vue b/src/Root.vue index c6a6099941..ea846c7498 100644 --- a/src/Root.vue +++ b/src/Root.vue @@ -10,6 +10,7 @@ import { provideTokens } from '@/providers/tokens.provider'; import { provideUserData } from '@/providers/user-data.provider'; import { provideWallets } from './providers/wallet.provider'; import { createProviderComponent } from './providers/createProviderComponent'; +import { provideCrossChainSync } from './providers/cross-chain-sync.provider'; // The other providers call useWallets so we need to provide it in a higher level const WalletsProvider = createProviderComponent(() => provideWallets()); @@ -18,6 +19,7 @@ const GlobalProvider = createProviderComponent(() => { const tokenLists = provideTokenLists(); provideTokens(userSettings, tokenLists); provideUserData(); + provideCrossChainSync(); }); /** diff --git a/src/__mocks__/transactions.ts b/src/__mocks__/transactions.ts index 8ec5b606e7..9ab8d1025c 100644 --- a/src/__mocks__/transactions.ts +++ b/src/__mocks__/transactions.ts @@ -1,3 +1,32 @@ +import BigNumber from 'bignumber.js'; + +const txReceiptMock = { + to: '0x1234567890abcdef', + from: '0xabcdef1234567890', + contractAddress: '0xabcdefabcdef1234567890', + transactionIndex: 1, + root: '0xabcdef1234567890abcdef', + gasUsed: new BigNumber('100000'), + logsBloom: '0xabcdef1234567890abcdef', + blockHash: '0xabcdef1234567890abcdef', + transactionHash: '0xabcdef1234567890abcdef', + logs: [ + { + // Sample log object + address: '0xabcdef1234567890abcdef', + data: '0xabcdef1234567890abcdef', + topics: ['0xabcdef1234567890abcdef', '0xabcdef1234567890abcdef'], + }, + ], + blockNumber: 1000, + confirmations: 10, + cumulativeGasUsed: new BigNumber('200000'), + effectiveGasPrice: new BigNumber('5000000000'), + byzantium: true, + type: 1, + status: 1, +}; + export const txResponseMock = { hash: '0xdac16ca21df7e4e2c9e013a7c1b8e1b869bf0bc6e97dde103e280703a60ff00d', type: 2, @@ -35,4 +64,5 @@ export const txResponseMock = { v: 1, creates: null, chainId: 0, + wait: () => txReceiptMock, }; diff --git a/src/assets/data/contracts/arbitrum.json b/src/assets/data/contracts/arbitrum.json index 8223eb0de2..46f32e263e 100644 --- a/src/assets/data/contracts/arbitrum.json +++ b/src/assets/data/contracts/arbitrum.json @@ -11,6 +11,7 @@ "ChildChainGauge": "0xa523f47A933D5020b23629dDf689695AA94612Dc", "ChildChainGaugeFactory": "0x6817149cb753BF529565B4D023d7507eD2ff4Bc0", "ChildChainGaugeRewardHelper": "0xA0DAbEBAAd1b243BBb243f933013d560819eB66f", + "ChildChainGaugeWorkingBalanceHelper": "0xEa924b45a3fcDAAdf4E5cFB1665823B8F8F2039B", "ChildChainGaugeTokenAdder": "0xbfD9769b061E57e478690299011A028194D66e3C", "ChildChainLiquidityGaugeFactory": "0xb08E16cFc07C684dAA2f93C70323BAdb2A6CBFd2", "ChildChainStreamer": "0xD7FAD3bd59D6477cbe1BE7f646F7f1BA25b230f8", diff --git a/src/assets/data/contracts/gnosis.json b/src/assets/data/contracts/gnosis.json index 2be087df51..5f3b716e0f 100644 --- a/src/assets/data/contracts/gnosis.json +++ b/src/assets/data/contracts/gnosis.json @@ -11,6 +11,7 @@ "ChildChainGauge": "0x96484f2aBF5e58b15176dbF1A799627B53F13B6d", "ChildChainGaugeFactory": "0x83E443EF4f9963C77bd860f94500075556668cb8", "ChildChainGaugeRewardHelper": "0xf7D5DcE55E6D47852F054697BAB6A1B48A00ddbd", + "ChildChainGaugeWorkingBalanceHelper": "0x682f0dDBFd41D1272982f64a499Fb62d80e27589", "ChildChainGaugeTokenAdder": "0x1802953277FD955f9a254B80Aa0582f193cF1d77", "ChildChainLiquidityGaugeFactory": "0x809B79b53F18E9bc08A961ED4678B901aC93213a", "ChildChainStreamer": "0x230a59F4d9ADc147480f03B0D3fFfeCd56c3289a", diff --git a/src/assets/data/contracts/optimism.json b/src/assets/data/contracts/optimism.json index 42e9646a6e..c02802f211 100644 --- a/src/assets/data/contracts/optimism.json +++ b/src/assets/data/contracts/optimism.json @@ -11,6 +11,7 @@ "ChildChainGauge": "0x81cFAE226343B24BA12EC6521Db2C79E7aeeb310", "ChildChainGaugeFactory": "0xa523f47A933D5020b23629dDf689695AA94612Dc", "ChildChainGaugeRewardHelper": "0x8aB784368A1883DA90D8513b48801e2Db1cb2D5D", + "ChildChainGaugeWorkingBalanceHelper": "0x9129E834e15eA19b6069e8f08a8EcFc13686B8dC", "ChildChainGaugeTokenAdder": "0x6f5a2eE11E7a772AeB5114A20d0D7c0ff61EB8A0", "ChildChainLiquidityGaugeFactory": "0x2E96068b3D5B5BAE3D7515da4A1D2E52d08A2647", "ChildChainStreamer": "0x239e55F427D44C3cc793f49bFB507ebe76638a2b", diff --git a/src/assets/data/contracts/polygon.json b/src/assets/data/contracts/polygon.json index a7d540eaed..ca6c8199d1 100644 --- a/src/assets/data/contracts/polygon.json +++ b/src/assets/data/contracts/polygon.json @@ -11,6 +11,7 @@ "ChildChainGauge": "0xc9b36096f5201ea332Db35d6D195774ea0D5988f", "ChildChainGaugeFactory": "0x22625eEDd92c81a219A83e1dc48f88d54786B017", "ChildChainGaugeRewardHelper": "0xaEb406b0E430BF5Ea2Dc0B9Fe62E4E53f74B3a33", + "ChildChainGaugeWorkingBalanceHelper": "0x08fd003D8F1892D4EC684E6C3EE0128081be461b", "ChildChainGaugeTokenAdder": "0x1554ee754707D5C93b7934AF404747Aba521Aaf2", "ChildChainLiquidityGaugeFactory": "0x3b8cA519122CdD8efb272b0D3085453404B25bD0", "ChildChainStreamer": "0x6f5a2eE11E7a772AeB5114A20d0D7c0ff61EB8A0", diff --git a/src/assets/images/icons/frame-loading.svg b/src/assets/images/icons/frame-loading.svg new file mode 100644 index 0000000000..11cd847e02 --- /dev/null +++ b/src/assets/images/icons/frame-loading.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/icons/networks/arbitrum-one.svg b/src/assets/images/icons/networks/arbitrum-one.svg index 58df2b690e..107986aa6f 100644 --- a/src/assets/images/icons/networks/arbitrum-one.svg +++ b/src/assets/images/icons/networks/arbitrum-one.svg @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + diff --git a/src/assets/images/icons/networks/arbitrum.svg b/src/assets/images/icons/networks/arbitrum.svg index 58df2b690e..107986aa6f 100644 --- a/src/assets/images/icons/networks/arbitrum.svg +++ b/src/assets/images/icons/networks/arbitrum.svg @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + diff --git a/src/assets/images/icons/networks/ethereum.svg b/src/assets/images/icons/networks/ethereum.svg index 0187ae68bd..c9306292dd 100644 --- a/src/assets/images/icons/networks/ethereum.svg +++ b/src/assets/images/icons/networks/ethereum.svg @@ -1,8 +1,15 @@ - - - - - - - + + + + + + + + + + + + + + diff --git a/src/assets/images/icons/networks/gnosis-chain.svg b/src/assets/images/icons/networks/gnosis-chain.svg index 835c1e024b..ac992de8f9 100644 --- a/src/assets/images/icons/networks/gnosis-chain.svg +++ b/src/assets/images/icons/networks/gnosis-chain.svg @@ -1,15 +1,6 @@ - - - - - - - + + + + + diff --git a/src/assets/images/icons/networks/goerli.svg b/src/assets/images/icons/networks/goerli.svg index ecd9b2bade..153e610f2d 100644 --- a/src/assets/images/icons/networks/goerli.svg +++ b/src/assets/images/icons/networks/goerli.svg @@ -1,8 +1,15 @@ - - - - - - - + + + + + + + + + + + + + + diff --git a/src/assets/images/icons/networks/homestead.svg b/src/assets/images/icons/networks/homestead.svg index 0187ae68bd..eabd4b7435 100644 --- a/src/assets/images/icons/networks/homestead.svg +++ b/src/assets/images/icons/networks/homestead.svg @@ -1,8 +1,15 @@ - - - - - - - + + + + + + + + + + + + + + diff --git a/src/assets/images/icons/networks/kovan.svg b/src/assets/images/icons/networks/kovan.svg index 0187ae68bd..47b91af828 100644 --- a/src/assets/images/icons/networks/kovan.svg +++ b/src/assets/images/icons/networks/kovan.svg @@ -1,8 +1,15 @@ - - - - - - - + + + + + + + + + + + + + + diff --git a/src/assets/images/icons/networks/optimism.svg b/src/assets/images/icons/networks/optimism.svg index 62afb2cc5c..95a688a1f6 100644 --- a/src/assets/images/icons/networks/optimism.svg +++ b/src/assets/images/icons/networks/optimism.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/src/assets/images/icons/networks/polygon.svg b/src/assets/images/icons/networks/polygon.svg index ca8b722ecc..d814d31232 100644 --- a/src/assets/images/icons/networks/polygon.svg +++ b/src/assets/images/icons/networks/polygon.svg @@ -1 +1,16 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + diff --git a/src/assets/images/icons/networks/zkevm.svg b/src/assets/images/icons/networks/zkevm.svg index 6e99ca7634..89c7882a3b 100644 --- a/src/assets/images/icons/networks/zkevm.svg +++ b/src/assets/images/icons/networks/zkevm.svg @@ -1 +1,39 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/icons/success-check.svg b/src/assets/images/icons/success-check.svg new file mode 100644 index 0000000000..d489f0601a --- /dev/null +++ b/src/assets/images/icons/success-check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/_global/BalAccordion/BalAccordion.vue b/src/components/_global/BalAccordion/BalAccordion.vue index 08ccc959c1..c1185796c6 100644 --- a/src/components/_global/BalAccordion/BalAccordion.vue +++ b/src/components/_global/BalAccordion/BalAccordion.vue @@ -17,10 +17,12 @@ type Props = { dependencies?: unknown; showSectionBorder?: boolean; reCalcKey?: number; + isOpenedByDefault?: boolean; }; const props = withDefaults(defineProps(), { showSectionBorder: true, + isOpenedByDefault: false, reCalcKey: 0, }); @@ -154,13 +156,13 @@ watch( />
-
+
{{ title }}

{{ description }} @@ -212,11 +216,11 @@ export default defineComponent({ } .bal-alert-container { - @apply flex; + @apply flex flex-grow; } .bal-alert-content { - @apply flex whitespace-pre-wrap; + @apply flex whitespace-pre-wrap flex-grow; min-width: 0; } diff --git a/src/components/_global/BalCheckbox/BalCheckbox.vue b/src/components/_global/BalCheckbox/BalCheckbox.vue index 56ebf43f8a..d702a56f1d 100644 --- a/src/components/_global/BalCheckbox/BalCheckbox.vue +++ b/src/components/_global/BalCheckbox/BalCheckbox.vue @@ -148,7 +148,7 @@ export default defineComponent({ } .bal-checkbox-input { - @apply text-blue-600 dark:text-blue-400 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 + @apply text-blue-600 dark:text-blue-400 bg-white dark:bg-gray-700 border border-gray-500 dark:border-gray-500 rounded leading-none; appearance: none; diff --git a/src/components/contextual/pages/claim/LegacyClaims.vue b/src/components/contextual/pages/claim/LegacyClaims.vue index 6825a7f7ba..899c082a0f 100644 --- a/src/components/contextual/pages/claim/LegacyClaims.vue +++ b/src/components/contextual/pages/claim/LegacyClaims.vue @@ -7,7 +7,7 @@ import useUserClaimsQuery from '@/composables/queries/useUserClaimsQuery'; import useEthers from '@/composables/useEthers'; import useNumbers, { FNumFormats } from '@/composables/useNumbers'; import { useTokens } from '@/providers/tokens.provider'; -import useTranasactionErrors from '@/composables/useTransactionErrors'; +import useTransactionErrors from '@/composables/useTransactionErrors'; import useTransactions from '@/composables/useTransactions'; import { TOKENS } from '@/constants/tokens'; import { bnum } from '@/lib/utils'; @@ -55,7 +55,7 @@ const { account, getProvider, isMismatchedNetwork } = useWeb3(); const { txListener } = useEthers(); const { addTransaction } = useTransactions(); const { priceFor, getToken } = useTokens(); -const { parseError } = useTranasactionErrors(); +const { parseError } = useTransactionErrors(); const BALTokenAddress = getAddress(TOKENS.Addresses.BAL); diff --git a/src/components/contextual/pages/pool/MyPoolBalancesCard.vue b/src/components/contextual/pages/pool/MyPoolBalancesCard.vue index 92690c5a27..1b113c6941 100644 --- a/src/components/contextual/pages/pool/MyPoolBalancesCard.vue +++ b/src/components/contextual/pages/pool/MyPoolBalancesCard.vue @@ -45,7 +45,7 @@ const { isMigratablePool } = usePoolHelpers(toRef(props, 'pool')); const { stakedShares } = usePoolStaking(); const { networkSlug } = useNetwork(); const router = useRouter(); -const { totalLockedValue } = useLock(); +const { totalLockedValue } = useLock({ enabled: isVeBalPool(props.pool.id) }); /** * COMPUTED diff --git a/src/components/contextual/pages/pool/PoolStatCards.vue b/src/components/contextual/pages/pool/PoolStatCards.vue index 2b12cb7ae6..f558e7f13f 100644 --- a/src/components/contextual/pages/pool/PoolStatCards.vue +++ b/src/components/contextual/pages/pool/PoolStatCards.vue @@ -8,6 +8,8 @@ import { isLBP, totalAprLabel } from '@/composables/usePoolHelpers'; import { APR_THRESHOLD, VOLUME_THRESHOLD } from '@/constants/pools'; import { Pool } from '@/services/pool/types'; import { AprBreakdown } from '@balancer-labs/sdk'; +import { useCrossChainSync } from '@/providers/cross-chain-sync.provider'; +import useNetwork from '@/composables/useNetwork'; /** * TYPES @@ -33,6 +35,8 @@ const props = withDefaults(defineProps(), { */ const { fNum } = useNumbers(); const { t } = useI18n(); +const { l2VeBalBalances } = useCrossChainSync(); +const { networkId } = useNetwork(); /** * COMPUTED @@ -44,6 +48,20 @@ const aprLabel = computed((): string => { return totalAprLabel(poolAPRs, props.pool?.boost); }); +const syncVeBalTooltip = computed(() => { + const vebalBalance = Number(l2VeBalBalances.value?.[networkId.value]); + + if (vebalBalance > 0) { + return 'Remember to resync if you have acquired more veBAL since your last sync, to get a higher boosted staking rate. Resync on the veBAL page on Ethereum Mainnet.'; + } + + if (vebalBalance === 0) { + return 'If you have veBAL, sync your balance on the veBAL page on Ethereum Mainnet to get higher boosted staking rates across L2 networks.'; + } + + return ''; +}); + const stats = computed(() => { const volumeSnapshot = Number(props.pool?.volumeSnapshot || '0'); const feesSnapshot = Number(props.pool?.feesSnapshot || '0'); @@ -80,6 +98,7 @@ const stats = computed(() => { ? '-' : aprLabel.value, loading: props.loadingApr, + tooltip: syncVeBalTooltip.value, }, ]; }); @@ -112,7 +131,12 @@ const stats = computed(() => { }, ]" > - {{ stat.value }} + {{ stat.value }} + + +

diff --git a/src/components/contextual/pages/pool/staking/StakingIncentivesCard.vue b/src/components/contextual/pages/pool/staking/StakingIncentivesCard.vue index 64ce2ed1e9..a1945d397c 100644 --- a/src/components/contextual/pages/pool/staking/StakingIncentivesCard.vue +++ b/src/components/contextual/pages/pool/staking/StakingIncentivesCard.vue @@ -14,6 +14,9 @@ import { usePoolStaking } from '@/providers/local/pool-staking.provider'; import { deprecatedDetails } from '@/composables/usePoolHelpers'; import { usePoolWarning } from '@/composables/usePoolWarning'; import { StakeAction } from './composables/useStakePreview'; +import StakingCardSyncAlert from '../../vebal/cross-chain-boost/StakingCardSyncAlert.vue'; +import useNetwork from '@/composables/useNetwork'; +import { Network } from '@/lib/config'; type Props = { pool: Pool; @@ -30,7 +33,7 @@ const emit = defineEmits<{ const isStakePreviewVisible = ref(false); const stakeAction = ref('stake'); const poolId = computed(() => props.pool.id); - +const isOpenedByDefault = ref(false); /** * COMPOSABLES */ @@ -44,6 +47,7 @@ const { hasNonPrefGaugeBalance, } = usePoolStaking(); const { isAffected } = usePoolWarning(poolId); +const { networkId } = useNetwork(); /** * COMPUTED @@ -105,6 +109,7 @@ function handlePreviewClose() { }, ]" :reCalcKey="hasNonPrefGaugeBalance ? 0 : 1" + :isOpenedByDefault="isOpenedByDefault" > diff --git a/src/components/contextual/pages/vebal/cross-chain-boost/CheckpointGaugeModal.vue b/src/components/contextual/pages/vebal/cross-chain-boost/CheckpointGaugeModal.vue new file mode 100644 index 0000000000..9966da26bb --- /dev/null +++ b/src/components/contextual/pages/vebal/cross-chain-boost/CheckpointGaugeModal.vue @@ -0,0 +1,87 @@ + + + diff --git a/src/components/contextual/pages/vebal/cross-chain-boost/CrossChainBoostCards.vue b/src/components/contextual/pages/vebal/cross-chain-boost/CrossChainBoostCards.vue new file mode 100644 index 0000000000..afeca6275f --- /dev/null +++ b/src/components/contextual/pages/vebal/cross-chain-boost/CrossChainBoostCards.vue @@ -0,0 +1,251 @@ + + + \ No newline at end of file diff --git a/src/components/contextual/pages/vebal/cross-chain-boost/CrossChainSyncModal.vue b/src/components/contextual/pages/vebal/cross-chain-boost/CrossChainSyncModal.vue new file mode 100644 index 0000000000..4c3245ba37 --- /dev/null +++ b/src/components/contextual/pages/vebal/cross-chain-boost/CrossChainSyncModal.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/src/components/contextual/pages/vebal/cross-chain-boost/IconLoaderWrapper.vue b/src/components/contextual/pages/vebal/cross-chain-boost/IconLoaderWrapper.vue new file mode 100644 index 0000000000..a9d0c2b7fd --- /dev/null +++ b/src/components/contextual/pages/vebal/cross-chain-boost/IconLoaderWrapper.vue @@ -0,0 +1,42 @@ + + + + + \ No newline at end of file diff --git a/src/components/contextual/pages/vebal/cross-chain-boost/PortfolioSyncTip.vue b/src/components/contextual/pages/vebal/cross-chain-boost/PortfolioSyncTip.vue new file mode 100644 index 0000000000..34e167cb45 --- /dev/null +++ b/src/components/contextual/pages/vebal/cross-chain-boost/PortfolioSyncTip.vue @@ -0,0 +1,71 @@ + + + \ No newline at end of file diff --git a/src/components/contextual/pages/vebal/cross-chain-boost/ProceedToSyncModal.vue b/src/components/contextual/pages/vebal/cross-chain-boost/ProceedToSyncModal.vue new file mode 100644 index 0000000000..a539a95ee5 --- /dev/null +++ b/src/components/contextual/pages/vebal/cross-chain-boost/ProceedToSyncModal.vue @@ -0,0 +1,45 @@ + + + diff --git a/src/components/contextual/pages/vebal/cross-chain-boost/StakingCardSyncAlert.vue b/src/components/contextual/pages/vebal/cross-chain-boost/StakingCardSyncAlert.vue new file mode 100644 index 0000000000..bd8ab325fb --- /dev/null +++ b/src/components/contextual/pages/vebal/cross-chain-boost/StakingCardSyncAlert.vue @@ -0,0 +1,134 @@ + + + \ No newline at end of file diff --git a/src/components/contextual/pages/vebal/cross-chain-boost/SyncFinalState.vue b/src/components/contextual/pages/vebal/cross-chain-boost/SyncFinalState.vue new file mode 100644 index 0000000000..56c6ae002a --- /dev/null +++ b/src/components/contextual/pages/vebal/cross-chain-boost/SyncFinalState.vue @@ -0,0 +1,93 @@ + + + diff --git a/src/components/contextual/pages/vebal/cross-chain-boost/SyncNetworkAction.vue b/src/components/contextual/pages/vebal/cross-chain-boost/SyncNetworkAction.vue new file mode 100644 index 0000000000..ea3f14e3e0 --- /dev/null +++ b/src/components/contextual/pages/vebal/cross-chain-boost/SyncNetworkAction.vue @@ -0,0 +1,177 @@ + + + diff --git a/src/components/contextual/pages/vebal/cross-chain-boost/SyncSelectNetwork.vue b/src/components/contextual/pages/vebal/cross-chain-boost/SyncSelectNetwork.vue new file mode 100644 index 0000000000..ed4a4c5569 --- /dev/null +++ b/src/components/contextual/pages/vebal/cross-chain-boost/SyncSelectNetwork.vue @@ -0,0 +1,93 @@ + + + diff --git a/src/components/forms/lock_actions/LockForm/components/LockPreviewModal/components/LockActions.vue b/src/components/forms/lock_actions/LockForm/components/LockPreviewModal/components/LockActions.vue index 01aded400c..5a6c4b55f3 100644 --- a/src/components/forms/lock_actions/LockForm/components/LockPreviewModal/components/LockActions.vue +++ b/src/components/forms/lock_actions/LockForm/components/LockPreviewModal/components/LockActions.vue @@ -24,6 +24,7 @@ import { VeBalLockInfo } from '@/services/balancer/contracts/contracts/veBAL'; import { ApprovalAction } from '@/composables/approvals/types'; import { captureException } from '@sentry/browser'; import useTokenApprovalActions from '@/composables/approvals/useTokenApprovalActions'; +import { isUserError } from '@/composables/useTransactionErrors'; /** * TYPES @@ -187,13 +188,15 @@ async function submit(lockType: LockType, actionIndex: number) { // An exception is already logged in balancerContractsService, but we should // log another here in case any exceptions are thrown before it's sent - captureException(error, { - level: 'fatal', - extra: { - lockType, - props, - }, - }); + if (!isUserError(error)) { + captureException(error, { + level: 'fatal', + extra: { + lockType, + props, + }, + }); + } return Promise.reject(error); } diff --git a/src/components/forms/pool_actions/AddLiquidityForm/AddLiquidityForm.vue b/src/components/forms/pool_actions/AddLiquidityForm/AddLiquidityForm.vue index f9ba3c5998..52c7586e93 100644 --- a/src/components/forms/pool_actions/AddLiquidityForm/AddLiquidityForm.vue +++ b/src/components/forms/pool_actions/AddLiquidityForm/AddLiquidityForm.vue @@ -125,7 +125,7 @@ function tokenOptions(address: string): string[] { return includesAddress( [wrappedNativeAsset.value.address, nativeAsset.address], address - ) && !isDeepPool.value + ) ? [wrappedNativeAsset.value.address, nativeAsset.address] : []; } diff --git a/src/components/heros/PortfolioPageHero.vue b/src/components/heros/PortfolioPageHero.vue index 18476beacb..2a73f2e7cd 100644 --- a/src/components/heros/PortfolioPageHero.vue +++ b/src/components/heros/PortfolioPageHero.vue @@ -1,6 +1,5 @@ @@ -81,6 +131,11 @@ const isLoadingTotalValue = computed((): boolean => isLoadingPools.value);
+ + diff --git a/src/components/layouts/DefaultLayout.vue b/src/components/layouts/DefaultLayout.vue new file mode 100644 index 0000000000..a442494b86 --- /dev/null +++ b/src/components/layouts/DefaultLayout.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/src/components/tables/PoolsTable/PoolsTable.vue b/src/components/tables/PoolsTable/PoolsTable.vue index 81b2fd1114..4945840874 100644 --- a/src/components/tables/PoolsTable/PoolsTable.vue +++ b/src/components/tables/PoolsTable/PoolsTable.vue @@ -256,14 +256,14 @@ function balanceValue(pool: Pool): string { } function boostFor(pool: Pool): string { - return props?.boosts?.[pool.id] || '1'; + return pool.boost || props?.boosts?.[pool.id] || '1'; } function aprLabelFor(pool: Pool): string { const poolAPRs = pool?.apr; if (!poolAPRs) return '0'; - return totalAprLabel(poolAPRs, pool.boost); + return totalAprLabel(poolAPRs, boostFor(pool)); } function lockedUntil(lockEndDate?: number) { diff --git a/src/components/tables/PoolsTable/PoolsTableActionsCell.vue b/src/components/tables/PoolsTable/PoolsTableActionsCell.vue index 1c87f64e13..2715f44803 100644 --- a/src/components/tables/PoolsTable/PoolsTableActionsCell.vue +++ b/src/components/tables/PoolsTable/PoolsTableActionsCell.vue @@ -60,7 +60,8 @@ const showVeBalLock = computed(() => isVeBalPool(props.pool.id)); diff --git a/src/composables/queries/useAllowancesQuery.spec.ts b/src/composables/queries/useAllowancesQuery.spec.ts index 1fbd0e67b2..0aa00e6eb7 100644 --- a/src/composables/queries/useAllowancesQuery.spec.ts +++ b/src/composables/queries/useAllowancesQuery.spec.ts @@ -42,7 +42,7 @@ test('Returns token allowances from balancer SDK', async () => { ]); const { result } = mountComposable(() => - useAllowancesQuery(tokens, spenders) + useAllowancesQuery({ tokens, contractAddresses: spenders }) ); const data = await waitForQueryData(result); diff --git a/src/composables/queries/useAllowancesQuery.ts b/src/composables/queries/useAllowancesQuery.ts index 82341e4088..b0276b9c67 100644 --- a/src/composables/queries/useAllowancesQuery.ts +++ b/src/composables/queries/useAllowancesQuery.ts @@ -14,15 +14,20 @@ import useNetwork from '../useNetwork'; */ type QueryResponse = ContractAllowancesMap; type QueryOptions = UseQueryOptions; +interface QueryInputs { + tokens: Ref; + contractAddresses: Ref; + isEnabled?: Ref; +} /** * Fetches all allowances for given tokens for each provided contract address. */ -export default function useAllowancesQuery( - tokens: Ref = ref({}), - contractAddresses: Ref = ref([]), - options: QueryOptions = {} -) { +export default function useAllowancesQuery({ + tokens, + contractAddresses, + isEnabled = ref(true), +}: QueryInputs) { /** * COMPOSABLES */ @@ -32,7 +37,7 @@ export default function useAllowancesQuery( /** * COMPUTED */ - const enabled = computed(() => isWalletReady.value); + const enabled = computed(() => isWalletReady.value && isEnabled.value); const tokenAddresses = computed(() => Object.keys(tokens.value)); /** @@ -62,7 +67,6 @@ export default function useAllowancesQuery( enabled, keepPreviousData: true, refetchOnWindowFocus: false, - ...options, }); return useQuery( diff --git a/src/composables/queries/useBalancesQuery.spec.ts b/src/composables/queries/useBalancesQuery.spec.ts index 1898737479..fca5e9d208 100644 --- a/src/composables/queries/useBalancesQuery.spec.ts +++ b/src/composables/queries/useBalancesQuery.spec.ts @@ -14,9 +14,7 @@ test('Returns token balances', async () => { [daiAddress]: aTokenInfo({ address: daiAddress }), }); - const { result } = mountComposable(() => - useBalancesQuery(tokens, { keepPreviousData: true }) - ); + const { result } = mountComposable(() => useBalancesQuery({ tokens })); const data = await waitForQueryData(result); diff --git a/src/composables/queries/useBalancesQuery.ts b/src/composables/queries/useBalancesQuery.ts index 06730810b9..f38b9e646a 100644 --- a/src/composables/queries/useBalancesQuery.ts +++ b/src/composables/queries/useBalancesQuery.ts @@ -15,14 +15,18 @@ import useNetwork from '../useNetwork'; */ type QueryResponse = BalanceMap; type QueryOptions = UseQueryOptions; +interface QueryInputs { + tokens: Ref; + isEnabled?: Ref; +} /** * Fetches all balances for provided tokens. */ -export default function useBalancesQuery( - tokens: Ref = ref({}), - options: QueryOptions = {} -) { +export default function useBalancesQuery({ + tokens, + isEnabled = ref(true), +}: QueryInputs) { /** * COMPOSABLES */ @@ -32,7 +36,7 @@ export default function useBalancesQuery( /** * COMPUTED */ - const enabled = computed(() => isWalletReady.value); + const enabled = computed(() => isWalletReady.value && isEnabled.value); const tokenAddresses = computed(() => Object.keys(tokens.value)); /** @@ -51,7 +55,6 @@ export default function useBalancesQuery( enabled, keepPreviousData: true, refetchOnWindowFocus: false, - ...options, }); return useQuery( diff --git a/src/composables/queries/useCrossChainNetwork.ts b/src/composables/queries/useCrossChainNetwork.ts new file mode 100644 index 0000000000..d6850ca3f6 --- /dev/null +++ b/src/composables/queries/useCrossChainNetwork.ts @@ -0,0 +1,114 @@ +import configs, { Network } from '@/lib/config'; +import { allEqual } from '@/lib/utils/array'; +import { NetworkSyncState } from '@/providers/cross-chain-sync.provider'; +import { OmniEscrowLock } from './useOmniEscrowLocksQuery'; +import { + VotingEscrowLock, + useVotingEscrowLocksQuery, +} from './useVotingEscrowQuery'; +import useWeb3 from '@/services/web3/useWeb3'; +import { bnum } from '@/lib/utils'; + +export function useCrossChainNetwork( + networkId: Network, + omniEscrowMap: ComputedRef | null> +) { + const { account } = useWeb3(); + + /** + * smart contracts can direct their veBAL boost to a different address on L2 + * for regular UI users, remoteUser will be the same as localUser + */ + const remoteUser = computed(() => { + if (networkId === Network.MAINNET) { + return account.value; + } + const layerZeroChainId = configs[networkId].layerZeroChainId || ''; + return omniEscrowMap.value?.[layerZeroChainId]?.remoteUser; + }); + + /** + * votingEscrowLocks contains the user's original veBAL data + * slope and bias is how a user's "balance" is stored on the smart contract + */ + const { + data: votingEscrowResponse, + refetch, + isError, + isInitialLoading: isLoading, + } = useVotingEscrowLocksQuery(networkId, remoteUser); + + const votingEscrowLocks = computed( + () => votingEscrowResponse.value?.votingEscrowLocks[0] + ); + + function getNetworkSyncState( + omniEscrowLock?: OmniEscrowLock | null, + mainnetEscrowLock?: VotingEscrowLock + ) { + if (!omniEscrowLock || !mainnetEscrowLock || !votingEscrowLocks.value) { + return NetworkSyncState.Unsynced; + } + + const biasOmni = omniEscrowLock.bias; + const slopeOmni = omniEscrowLock.slope; + + const biasMainnet = mainnetEscrowLock.bias; + const slopeMainnet = mainnetEscrowLock.slope; + + const biasNetwork = votingEscrowLocks.value.bias; + const slopeNetwork = votingEscrowLocks.value.slope; + + if (!omniEscrowLock.slope || !mainnetEscrowLock.slope || !slopeNetwork) + return NetworkSyncState.Unsynced; + + const isSynced = + allEqual([biasOmni, biasMainnet, biasNetwork]) && + allEqual([slopeOmni, slopeMainnet, slopeNetwork]); + + const isSyncing = + allEqual([biasOmni, biasMainnet]) && + allEqual([slopeOmni, slopeMainnet]) && + slopeOmni !== slopeNetwork && + biasOmni !== biasNetwork; + + if (isSynced) { + return NetworkSyncState.Synced; + } + + if (isSyncing) { + return NetworkSyncState.Syncing; + } + + return NetworkSyncState.Unsynced; + } + + // veBAL_balance = bias - slope * (now() - timestamp) + function calculateVeBAlBalance() { + const bias = votingEscrowLocks.value?.bias; + const slope = votingEscrowLocks.value?.slope; + const timestamp = votingEscrowLocks.value?.timestamp; + + if (!bias || !slope || !timestamp) return bnum(0).toFixed(4).toString(); + + const x = bnum(slope).multipliedBy( + Math.floor(Date.now() / 1000) - timestamp + ); + + if (x.isLessThan(0)) return bnum(bias).toFixed(4).toString(); + + const balance = bnum(bias).minus(x); + if (balance.isLessThan(0)) return bnum(0).toFixed(4).toString(); + + return balance.toFixed(4).toString(); + } + + return { + getNetworkSyncState, + votingEscrowLocks, + refetch, + calculateVeBAlBalance, + isLoading, + isError, + }; +} diff --git a/src/composables/queries/useHistoricalPricesQuery.ts b/src/composables/queries/useHistoricalPricesQuery.ts index ca6e5b6887..21e540a3ec 100644 --- a/src/composables/queries/useHistoricalPricesQuery.ts +++ b/src/composables/queries/useHistoricalPricesQuery.ts @@ -64,12 +64,21 @@ export default function useHistoricalPricesQuery( */ const aggregateBy = shapshotDaysNum <= 90 ? 'hour' : 'day'; - return await coingeckoService.prices.getTokensHistorical( - tokensList, - shapshotDaysNum, - 1, - aggregateBy - ); + // if the coingecko query fails for this query key, we can pretty safely assume it'll keep failing + // by returning an empty object we signal to stop retrying this hook. + try { + const historicalPrices = + await coingeckoService.prices.getTokensHistorical( + tokensList, + shapshotDaysNum, + 1, + aggregateBy + ); + + return historicalPrices; + } catch { + return {}; + } }; const queryOptions = reactive({ diff --git a/src/composables/queries/useOmniEscrowLocksQuery.ts b/src/composables/queries/useOmniEscrowLocksQuery.ts new file mode 100644 index 0000000000..77c78b8cc3 --- /dev/null +++ b/src/composables/queries/useOmniEscrowLocksQuery.ts @@ -0,0 +1,61 @@ +import QUERY_KEYS from '@/constants/queryKeys'; +import useGraphQuery from './useGraphQuery'; +import useNetwork from '../useNetwork'; +import config, { Network } from '@/lib/config'; + +const attrs = { + id: true, + localUser: { + id: true, + }, + remoteUser: true, + bias: true, + slope: true, + dstChainId: true, +}; + +export interface OmniEscrowLock { + id: string; + localUser: { + id: string; + }; + remoteUser: string; + bias: string; + slope: string; + dstChainId: string; +} + +export interface OmniEscrowLockResponse { + omniVotingEscrowLocks: OmniEscrowLock[]; +} + +export function useOmniEscrowLocksQuery(account: ComputedRef) { + const { networkId } = useNetwork(); + + const useOmniEscrowLocksQueryEnabled = computed(() => !!account.value); + + /** + * QUERY INPUTS + */ + const queryKey = QUERY_KEYS.Gauges.OmniEscrowLocks(networkId, account); + + return useGraphQuery( + config[Network.MAINNET].subgraphs.gauge, + queryKey, + () => ({ + __name: 'OmniEscrowLocks', + omniVotingEscrowLocks: { + __args: { + where: { + localUser: account.value?.toLowerCase(), + }, + }, + ...attrs, + }, + }), + reactive({ + enabled: useOmniEscrowLocksQueryEnabled, + refetchOnWindowFocus: false, + }) + ); +} diff --git a/src/composables/queries/usePoolQuery.ts b/src/composables/queries/usePoolQuery.ts index 2b3094c446..51f39b6dfd 100644 --- a/src/composables/queries/usePoolQuery.ts +++ b/src/composables/queries/usePoolQuery.ts @@ -79,7 +79,7 @@ export default function usePoolQuery( } // Inject pool tokens into token registry - await injectTokens([ + injectTokens([ ...tokensListExclBpt(pool), ...tokenTreeLeafs(pool.tokens), pool.address, // We need to inject pool addresses so we can fetch a user's balance for that pool. @@ -90,6 +90,8 @@ export default function usePoolQuery( const queryOptions = reactive({ enabled, + keepPreviousData: true, + refetchOnWindowFocus: false, ...options, }); diff --git a/src/composables/queries/useUserBoostsQuery.spec.ts b/src/composables/queries/useUserBoostsQuery.spec.ts index 4a3e66ceb5..09fabcc6d0 100644 --- a/src/composables/queries/useUserBoostsQuery.spec.ts +++ b/src/composables/queries/useUserBoostsQuery.spec.ts @@ -26,5 +26,5 @@ test('Does not calculate boosts when user does not have gauge shares', async () const data = await waitForQueryData(result); - expect(data).toEqual({ 'test pool id': '1.00000000833325' }); + expect(data).toEqual({ 'test pool id': '1.0854950634314737561' }); }); diff --git a/src/composables/queries/useVotingEscrowQuery.ts b/src/composables/queries/useVotingEscrowQuery.ts new file mode 100644 index 0000000000..4d0b455c59 --- /dev/null +++ b/src/composables/queries/useVotingEscrowQuery.ts @@ -0,0 +1,62 @@ +import QUERY_KEYS from '@/constants/queryKeys'; +import useGraphQuery from './useGraphQuery'; +import useWeb3 from '@/services/web3/useWeb3'; +import config, { Network } from '@/lib/config'; + +export interface VotingEscrowLock { + id: string; + slope: string; + bias: string; + timestamp: number; +} + +export interface VotingEscrowLockResponse { + votingEscrowLocks: VotingEscrowLock[]; +} + +const attrs = { + id: true, + bias: true, + slope: true, + timestamp: true, +}; + +export function useVotingEscrowLocksQuery( + networkId: Network, + user: ComputedRef +) { + const { account } = useWeb3(); + + const votingEscrowLocksQueryEnabled = computed(() => { + if (!account.value) { + return false; + } + + if (networkId === Network.MAINNET) { + return true; + } + + // we need remote user for l2s + return !!user.value; + }); + + return useGraphQuery( + config[networkId].subgraphs.gauge, + QUERY_KEYS.Gauges.VotingEscrowLocksByNetworkId(networkId, account, user), + () => ({ + __name: 'VotingEscrowLocks', + votingEscrowLocks: { + __args: { + where: { + user: user.value?.toLowerCase(), + }, + }, + ...attrs, + }, + }), + reactive({ + enabled: votingEscrowLocksQueryEnabled, + refetchOnWindowFocus: false, + }) + ); +} diff --git a/src/composables/swap/useCowswap.ts b/src/composables/swap/useCowswap.ts index 1b24748222..18260a9cc3 100644 --- a/src/composables/swap/useCowswap.ts +++ b/src/composables/swap/useCowswap.ts @@ -21,7 +21,7 @@ import useTransactions from '../useTransactions'; import { SwapQuote } from './types'; import { captureException } from '@sentry/browser'; import { Goals, trackGoal } from '../useFathom'; -import useTranasactionErrors from '../useTransactionErrors'; +import { isUserError } from '../useTransactionErrors'; import { useI18n } from 'vue-i18n'; import { useApp } from '@/composables/useApp'; @@ -93,7 +93,6 @@ export default function useCowswap({ const { addTransaction } = useTransactions(); const { fNum } = useNumbers(); const { balanceFor } = useTokens(); - const { isUserRejected } = useTranasactionErrors(); const { t } = useI18n(); // DATA @@ -238,7 +237,7 @@ export default function useCowswap({ confirming.value = false; trackGoal(Goals.CowswapSwap); } catch (error) { - if (!isUserRejected(error)) { + if (!isUserError(error)) { console.trace(error); state.submissionError = t('swapException', ['Cowswap']); captureException(new Error(state.submissionError, { cause: error }), { diff --git a/src/composables/swap/useJoinExit.ts b/src/composables/swap/useJoinExit.ts index 8e1f8b9668..587ef7ef67 100644 --- a/src/composables/swap/useJoinExit.ts +++ b/src/composables/swap/useJoinExit.ts @@ -28,7 +28,7 @@ import useEthers from '../useEthers'; import useRelayerApprovalQuery from '@/composables/queries/useRelayerApprovalQuery'; import { TransactionBuilder } from '@/services/web3/transactions/transaction.builder'; import BatchRelayerAbi from '@/lib/abi/BatchRelayer.json'; -import useTranasactionErrors from '../useTransactionErrors'; +import { isUserError } from '../useTransactionErrors'; import { useI18n } from 'vue-i18n'; type JoinExitState = { @@ -91,7 +91,6 @@ export default function useJoinExit({ const { addTransaction } = useTransactions(); const { txListener } = useEthers(); const { fNum } = useNumbers(); - const { isUserRejected } = useTranasactionErrors(); const { t } = useI18n(); const hasValidationError = computed( @@ -237,7 +236,7 @@ export default function useJoinExit({ }, }); } catch (error) { - if (!isUserRejected(error)) { + if (!isUserError(error)) { console.trace(error); state.submissionError = t('swapException', ['Relayer']); captureException(new Error(state.submissionError, { cause: error }), { diff --git a/src/composables/swap/useSor.ts b/src/composables/swap/useSor.ts index 8d818eca52..22ebdb4fba 100644 --- a/src/composables/swap/useSor.ts +++ b/src/composables/swap/useSor.ts @@ -43,7 +43,7 @@ import useTransactions, { TransactionAction } from '../useTransactions'; import { SwapQuote } from './types'; import { captureException } from '@sentry/browser'; import { overflowProtected } from '@/components/_global/BalTextInput/helpers'; -import useTranasactionErrors from '../useTransactionErrors'; +import { isUserError } from '../useTransactionErrors'; type SorState = { validationErrors: { @@ -204,7 +204,6 @@ export default function useSor({ const { fNum, toFiat } = useNumbers(); const { t } = useI18n(); const { injectTokens, priceFor, getToken } = useTokens(); - const { isUserRejected } = useTranasactionErrors(); const { swapIn, swapOut } = useSwapper(); onMounted(async () => { @@ -308,10 +307,10 @@ export default function useSor({ tokenOutAddress.toLowerCase() ); - let tokenInAmount = BigNumber.from(deltas[tokenInPosition]).abs(); - let tokenOutAmount = BigNumber.from(deltas[tokenOutPosition]).abs(); - if (swapType === SwapType.SwapExactOut) { + let tokenInAmount = deltas[tokenInPosition] + ? BigNumber.from(deltas[tokenInPosition]).abs() + : BigNumber.from(0); tokenInAmount = await mutateAmount({ amount: tokenInAmount, address: tokenInAddressInput.value, @@ -324,6 +323,9 @@ export default function useSor({ } if (swapType === SwapType.SwapExactIn) { + let tokenOutAmount = deltas[tokenOutPosition] + ? BigNumber.from(deltas[tokenOutPosition]).abs() + : BigNumber.from(0); tokenOutAmount = await mutateAmount({ amount: tokenOutAmount, address: tokenOutAddressInput.value, @@ -796,7 +798,7 @@ export default function useSor({ } function handleSwapException(error: Error) { - if (!isUserRejected(error)) { + if (!isUserError(error)) { console.trace(error); state.submissionError = t('swapException', ['Balancer']); captureException(new Error(state.submissionError, { cause: error })); diff --git a/src/composables/useLock.ts b/src/composables/useLock.ts index dacdae5097..688d4e178e 100644 --- a/src/composables/useLock.ts +++ b/src/composables/useLock.ts @@ -6,6 +6,7 @@ import { useUserData } from '@/providers/user-data.provider'; import usePoolQuery from './queries/usePoolQuery'; import { fiatValueOf } from './usePoolHelpers'; import useVeBal, { isVeBalSupported } from './useVeBAL'; +import { bnum } from '@/lib/utils'; interface Options { enabled?: boolean; @@ -15,7 +16,7 @@ export function useLock({ enabled = true }: Options = {}) { * COMPOSABLES */ const { lockablePoolId } = useVeBal(); - const { getToken } = useTokens(); + const { getToken, balanceFor } = useTokens(); /** * QUERIES @@ -64,6 +65,20 @@ export function useLock({ enabled = true }: Options = {}) { : '0' ); + const bptPrice = computed(() => { + if (!lockPool.value) return bnum(0); + return bnum(lockPool.value.totalLiquidity).div(lockPool.value.totalShares); + }); + + const bptBalance = computed(() => { + if (!lockPool.value) return bnum(0); + return balanceFor(lockPool.value.address); + }); + + const fiatTotal = computed(() => + bptPrice.value.times(bptBalance.value).toString() + ); + return { isLoadingLockPool, isLoadingLockInfo, @@ -73,5 +88,7 @@ export function useLock({ enabled = true }: Options = {}) { lock, totalLockedValue, totalLockedShares, + bptBalance, + fiatTotal, }; } diff --git a/src/composables/userTransactionErrors.spec.ts b/src/composables/useTransactionErrors.spec.ts similarity index 58% rename from src/composables/userTransactionErrors.spec.ts rename to src/composables/useTransactionErrors.spec.ts index 373e2e3ffc..bb39031d81 100644 --- a/src/composables/userTransactionErrors.spec.ts +++ b/src/composables/useTransactionErrors.spec.ts @@ -1,7 +1,7 @@ import { WalletError } from '@/types'; import { isUserRejected } from './useTransactionErrors'; -describe('userTransactionErrors', () => { +describe('useTransactionErrors', () => { describe('isUserRejected', () => { it('Should return false for a non-user error', () => { const error = new Error('Unsupported Exit Type For Pool'); @@ -33,5 +33,30 @@ describe('userTransactionErrors', () => { rejectionError.code = 4001; expect(isUserRejected(rejectionError)).toBe(true); }); + + // See https://balancer-labs.sentry.io/issues/4199718124/events/74a6db95ab424cd6a286af7a00076d2c/ + it('Should return true if the error is an object with a and b parameters', () => { + const rejectionError = { a: -500, b: 'Cancelled by User' }; + expect(isUserRejected(rejectionError)).toBe(true); + }); + + // See https://balancer-labs.sentry.io/issues/4199718124/events/f1a41824e66141b4806c50db5f081f7b/ + it('Should return true if its a user error where they are out of gas', () => { + const rejectionError = { + code: 5002, + message: + "User rejected methods. Your wallet doesn't have enough Ethereum to start this transfer.", + }; + expect(isUserRejected(rejectionError)).toBe(true); + }); + + // See https://balancer-labs.sentry.io/issues/4199718124/events/57d26b71647046f2be3620f3c0165714/ + it('Should return true if its a user error as an object', () => { + const rejectionError = { + code: 5001, + message: 'User disapproved requested methods', + }; + expect(isUserRejected(rejectionError)).toBe(true); + }); }); }); diff --git a/src/composables/useTransactionErrors.ts b/src/composables/useTransactionErrors.ts index 7b29ce024c..0ae09fca6a 100644 --- a/src/composables/useTransactionErrors.ts +++ b/src/composables/useTransactionErrors.ts @@ -3,51 +3,66 @@ import { useI18n } from 'vue-i18n'; import { TransactionError } from '@/types/transactions'; export function isUserRejected(error): boolean { - if (!error) return false; - - const userRejectionMessages = [ - 'user rejected transaction', - 'request rejected', - 'user rejected methods.', - 'user rejected the transaction', - 'rejected by user', - 'user canceled', - 'cancelled by user', - 'transaction declined', - 'transaction was rejected', - 'user denied transaction signature', + const messages = [ + /user rejected transaction/, + /request rejected/, + /user rejected methods./, + /user rejected the transaction/, + /rejected by user/, + /user canceled/, + /cancelled by user/, + /transaction declined/, + /transaction was rejected/, + /user denied transaction signature/, + /user disapproved requested methods/, + /canceled/, ]; + return isErrorType(error, messages); +} + +export function isUserNotEnoughGas(error): boolean { + const messages = [/insufficient funds for gas/]; + + return isErrorType(error, messages); +} + +function isErrorType(error, messages: RegExp[]): boolean { + if (!error) return false; + if ( typeof error === 'string' && - userRejectionMessages.includes(error.toLowerCase()) + messages.some(msg => msg.test(error.toLowerCase())) ) return true; if ( error.message && - userRejectionMessages.includes(error.message.toLowerCase()) + messages.some(msg => msg.test(error.message.toLowerCase())) ) return true; if ( typeof error.reason === 'string' && - userRejectionMessages.includes(error.reason.toLowerCase()) + messages.some(msg => msg.test(error.reason.toLowerCase())) ) return true; if ( error.cause?.message && - userRejectionMessages.includes(error.cause.message.toLowerCase()) + messages.some(msg => msg.test(error.cause.message.toLowerCase())) ) return true; if ( typeof error.cause === 'string' && - userRejectionMessages.includes(error.cause.toLowerCase()) + messages.some(msg => msg.test(error.cause.toLowerCase())) ) return true; + if (error.b && messages.some(msg => msg.test(error.b.toLowerCase()))) + return true; + if (error?.code && error.code === 4001) { return true; } @@ -57,7 +72,12 @@ export function isUserRejected(error): boolean { return false; } -export default function useTranasactionErrors() { +// Errors that are caused by the user or the state of their wallet. +export function isUserError(error): boolean { + return isUserRejected(error) || isUserNotEnoughGas(error); +} + +export default function useTransactionErrors() { /** * COMPOSABLES */ @@ -92,7 +112,7 @@ export default function useTranasactionErrors() { * METHODS */ function parseError(error): TransactionError | null { - if (isUserRejected(error)) return null; // User rejected transaction + if (isUserError(error)) return null; if (error?.code && error.code === 'UNPREDICTABLE_GAS_LIMIT') return cannotEstimateGasError; diff --git a/src/composables/useTransactions.ts b/src/composables/useTransactions.ts index cddd4f11ad..1d3b25fd25 100644 --- a/src/composables/useTransactions.ts +++ b/src/composables/useTransactions.ts @@ -48,7 +48,9 @@ export type TransactionAction = | 'voteForGauge' | 'unstake' | 'stake' - | 'restake'; + | 'restake' + | 'sync' + | 'userGaugeCheckpoint'; export type TransactionType = 'order' | 'tx'; diff --git a/src/composables/useVotingGauges.spec.ts b/src/composables/useVotingGauges.spec.ts index b00f88373c..4254ddc9b3 100644 --- a/src/composables/useVotingGauges.spec.ts +++ b/src/composables/useVotingGauges.spec.ts @@ -7,10 +7,6 @@ vi.mock('@/services/web3/useWeb3'); describe('useVotingGauges', () => { describe('votingPeriodEnd', () => { - beforeAll(() => { - vi.useFakeTimers(); - }); - it('Should work for an arbitrary time', () => { vi.setSystemTime(Date.UTC(2022, 3, 23, 15, 24, 38)); // Sun Apr 23 2022 15:24:38 GMT+0000 const { result } = mount(() => useVotingGauges()); @@ -45,9 +41,5 @@ describe('useVotingGauges', () => { const { votingPeriodEnd } = result; expect(votingPeriodEnd.value).toEqual([6, 23, 59, 59]); }); - - afterAll(() => { - vi.useRealTimers(); - }); }); }); diff --git a/src/composables/useVotingGauges.ts b/src/composables/useVotingGauges.ts index 601a093513..e3798b04dc 100644 --- a/src/composables/useVotingGauges.ts +++ b/src/composables/useVotingGauges.ts @@ -36,9 +36,8 @@ export default function useVotingGauges() { const _votingGauges = computed((): VotingGauge[] => { if (isGoerli.value) { return GOERLI_VOTING_GAUGES as VotingGauge[]; - } else { - return MAINNET_VOTING_GAUGES as VotingGauge[]; } + return MAINNET_VOTING_GAUGES as VotingGauge[]; }); // Fetch onchain votes data for given votingGauges diff --git a/src/constants/queryKeys.ts b/src/constants/queryKeys.ts index e0b42d7d4c..16a42fbb45 100644 --- a/src/constants/queryKeys.ts +++ b/src/constants/queryKeys.ts @@ -247,6 +247,15 @@ const QUERY_KEYS = { 'votingEscrowLocks', lockedAmount, ], + VotingEscrowLocksByNetworkId: ( + networkId: Network, + account: Ref, + providedUser: Ref + ) => ['votingEscrowLocksByNetworkId', { networkId, account, providedUser }], + OmniEscrowLocks: (networkId: Ref, account: Ref) => [ + 'omniEscrowLocks', + { account, networkId }, + ], Voting: (account: Ref) => ['gauges', 'voting', { account }], }, Transaction: { diff --git a/src/constants/symbol.keys.ts b/src/constants/symbol.keys.ts index 0caf7f6504..33153b9ac8 100644 --- a/src/constants/symbol.keys.ts +++ b/src/constants/symbol.keys.ts @@ -13,5 +13,6 @@ export default { Wallets: 'provider.wallets', Pool: 'provider.pool', UserTokens: 'provider.userTokens', + CrossChainSync: 'provider.crossChainSync', }, }; diff --git a/src/dependencies/EthersContract.mocks.ts b/src/dependencies/EthersContract.mocks.ts index e3d631da5e..ba5fbde532 100644 --- a/src/dependencies/EthersContract.mocks.ts +++ b/src/dependencies/EthersContract.mocks.ts @@ -7,6 +7,7 @@ export const defaultAdjustedBalance = '55555'; export const defaultBatchSwapResponse = 'Batch Swap response'; +export const defaultTotalSupply = '9747054'; export const defaultContractBalance = '321'; export const defaultContractBalanceBN = BigNumber.from(defaultContractBalance); interface IContract { @@ -33,9 +34,16 @@ export class MockedContractWithSigner implements IContract { adjustedBalanceOf() { return defaultAdjustedBalance; } + totalSupply() { + return Promise.resolve(defaultTotalSupply); + } balanceOf() { return Promise.resolve(defaultContractBalanceBN); } + + connect() { + return this; + } } export function initEthersContractWithDefaultMocks() { diff --git a/src/dependencies/contract.concern.mocks.ts b/src/dependencies/contract.concern.mocks.ts index 71b7bb86ef..9b0770e43e 100644 --- a/src/dependencies/contract.concern.mocks.ts +++ b/src/dependencies/contract.concern.mocks.ts @@ -1,12 +1,17 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { // eslint-disable-next-line no-restricted-imports ContractConcern, SendTransactionOpts, } from '@/services/web3/transactions/concerns/contract.concern'; -import { initContractConcern } from './contract.concern'; -import { aSigner } from '@tests/unit/builders/signer'; import { TransactionResponse } from '@ethersproject/abstract-provider'; +import { BigNumber } from '@ethersproject/bignumber'; import { mock } from 'vitest-mock-extended'; +import { initContractConcern } from './contract.concern'; + +export const defaultCallStaticResponse = { + nativeFee: BigNumber.from(1), +}; export const defaultContractTransactionHash = '0x0679d36034a11eb150a807e9aa648ed79ecdcf7f3fe5ec3cbad9123e67b02c96'; @@ -20,11 +25,15 @@ export const sendTransactionMock = vi.fn( Promise.resolve(defaultContractTransactionResponse) ); -export class MockedContractConcern extends ContractConcern { - constructor() { - super(aSigner()); - } +export const callStaticMock = vi.fn( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (opts: SendTransactionOpts) => Promise.resolve(defaultCallStaticResponse) +); +export class MockedContractConcern extends ContractConcern { + callStatic = (opts: SendTransactionOpts): Promise => + //@ts-ignore + callStaticMock(opts); sendTransaction = (opts: SendTransactionOpts) => sendTransactionMock(opts); } diff --git a/src/lib/abi/GaugeWorkingBalanceHelper.json b/src/lib/abi/GaugeWorkingBalanceHelper.json new file mode 100644 index 0000000000..cf66ae22b5 --- /dev/null +++ b/src/lib/abi/GaugeWorkingBalanceHelper.json @@ -0,0 +1,73 @@ +[ + { + "inputs": [ + { + "internalType": "contract IVeDelegationProxy", + "name": "veDelegationProxy", + "type": "address" + }, + { + "internalType": "bool", + "name": "readTotalSupplyFromVE", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "getVotingEscrow", + "outputs": [ + { "internalType": "contract IERC20", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getVotingEscrowDelegationProxy", + "outputs": [ + { + "internalType": "contract IVeDelegation", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract IGauge", "name": "gauge", "type": "address" }, + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "getWorkingBalanceToSupplyRatios", + "outputs": [ + { "internalType": "uint256", "name": "", "type": "uint256" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract IGauge", "name": "gauge", "type": "address" }, + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "getWorkingBalances", + "outputs": [ + { "internalType": "uint256", "name": "", "type": "uint256" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "readsTotalSupplyFromVE", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + } +] diff --git a/src/lib/abi/OmniVotingEscrow.json b/src/lib/abi/OmniVotingEscrow.json new file mode 100644 index 0000000000..ad25db1b8e --- /dev/null +++ b/src/lib/abi/OmniVotingEscrow.json @@ -0,0 +1,187 @@ +[ + { + "inputs": [ + { "internalType": "contract IVault", "name": "vault", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes", + "name": "newAdapterParams", + "type": "bytes" + } + ], + "name": "AdapterParamsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IOmniVotingEscrow", + "name": "newOmniVotingEscrow", + "type": "address" + } + ], + "name": "OmniVotingEscrowUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "newUseZero", + "type": "bool" + } + ], + "name": "UseZeroUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "newZeroPaymentAddress", + "type": "address" + } + ], + "name": "ZeroPaymentAddressUpdated", + "type": "event" + }, + { + "inputs": [ + { "internalType": "uint16", "name": "_dstChainId", "type": "uint16" } + ], + "name": "estimateSendUserBalance", + "outputs": [ + { "internalType": "uint256", "name": "nativeFee", "type": "uint256" }, + { "internalType": "uint256", "name": "zroFee", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes4", "name": "selector", "type": "bytes4" } + ], + "name": "getActionId", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAdapterParams", + "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAuthorizer", + "outputs": [ + { "internalType": "contract IAuthorizer", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getOmniVotingEscrow", + "outputs": [ + { + "internalType": "contract IOmniVotingEscrow", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getUseZero", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getVault", + "outputs": [ + { "internalType": "contract IVault", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getZeroPaymentAddress", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "_user", "type": "address" }, + { "internalType": "uint16", "name": "_dstChainId", "type": "uint16" }, + { + "internalType": "address payable", + "name": "_refundAddress", + "type": "address" + } + ], + "name": "sendUserBalance", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes", "name": "adapterParams", "type": "bytes" } + ], + "name": "setAdapterParams", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IOmniVotingEscrow", + "name": "omniVotingEscrow", + "type": "address" + } + ], + "name": "setOmniVotingEscrow", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "bool", "name": "useZro", "type": "bool" }], + "name": "setUseZero", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "paymentAddress", "type": "address" } + ], + "name": "setZeroPaymentAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/lib/abi/veDelegationProxyL2.json b/src/lib/abi/veDelegationProxyL2.json new file mode 100644 index 0000000000..0c79003d8a --- /dev/null +++ b/src/lib/abi/veDelegationProxyL2.json @@ -0,0 +1,126 @@ +[ + { + "inputs": [ + { "internalType": "contract IVault", "name": "vault", "type": "address" }, + { + "internalType": "contract IERC20", + "name": "votingEscrow", + "type": "address" + }, + { + "internalType": "contract IVeDelegation", + "name": "delegation", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "DelegationImplementationUpdated", + "type": "event" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "adjustedBalanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "adjusted_balance_of", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "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": "getDelegationImplementation", + "outputs": [ + { + "internalType": "contract IVeDelegation", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getVault", + "outputs": [ + { "internalType": "contract IVault", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getVotingEscrow", + "outputs": [ + { "internalType": "contract IERC20", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "killDelegation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IVeDelegation", + "name": "delegation", + "type": "address" + } + ], + "name": "setDelegation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + } +] diff --git a/src/lib/config/arbitrum/contracts.ts b/src/lib/config/arbitrum/contracts.ts index ff7c738f7e..10bed591a5 100644 --- a/src/lib/config/arbitrum/contracts.ts +++ b/src/lib/config/arbitrum/contracts.ts @@ -18,12 +18,13 @@ const contracts: Contracts = { gaugeController: '', tokenAdmin: '', veBAL: '', - veDelegationProxy: '', + veDelegationProxy: '0x81cFAE226343B24BA12EC6521Db2C79E7aeeb310', veBALHelpers: '', feeDistributor: '', feeDistributorDeprecated: '', faucet: '', gaugeRewardsHelper: arbitrum.ChildChainGaugeRewardHelper, + gaugeWorkingBalanceHelper: arbitrum.ChildChainGaugeWorkingBalanceHelper, }; export default contracts; diff --git a/src/lib/config/arbitrum/index.ts b/src/lib/config/arbitrum/index.ts index d917cae95c..d742cb60df 100644 --- a/src/lib/config/arbitrum/index.ts +++ b/src/lib/config/arbitrum/index.ts @@ -9,6 +9,8 @@ import rateProviders from './rateProviders'; const config: Config = { key: '42161', chainId: 42161, + layerZeroChainId: 110, + supportsVeBalSync: true, chainName: 'Arbitrum', name: 'Arbitrum', shortName: 'Arbitrum', diff --git a/src/lib/config/arbitrum/pools.ts b/src/lib/config/arbitrum/pools.ts index a9fa73a2a4..7c1bd9a9d8 100644 --- a/src/lib/config/arbitrum/pools.ts +++ b/src/lib/config/arbitrum/pools.ts @@ -8,7 +8,7 @@ const pools: Pools = { PerPool: 10, PerPoolInitial: 5, }, - BoostsEnabled: false, + BoostsEnabled: true, DelegateOwner: '0xba1ba1ba1ba1ba1ba1ba1ba1ba1ba1ba1ba1ba1b', ZeroAddress: '0x0000000000000000000000000000000000000000', DynamicFees: { diff --git a/src/lib/config/gnosis-chain/contracts.ts b/src/lib/config/gnosis-chain/contracts.ts index 596216a381..4575b069e1 100644 --- a/src/lib/config/gnosis-chain/contracts.ts +++ b/src/lib/config/gnosis-chain/contracts.ts @@ -17,12 +17,13 @@ const contracts: Contracts = { gaugeController: '', tokenAdmin: '', veBAL: '', - veDelegationProxy: '', + veDelegationProxy: '0x7A2535f5fB47b8e44c02Ef5D9990588313fe8F05', veBALHelpers: '', feeDistributor: '', feeDistributorDeprecated: '', faucet: '', gaugeRewardsHelper: gnosis.ChildChainGaugeRewardHelper, + gaugeWorkingBalanceHelper: gnosis.ChildChainGaugeWorkingBalanceHelper, }; export default contracts; diff --git a/src/lib/config/gnosis-chain/index.ts b/src/lib/config/gnosis-chain/index.ts index 3ca95fdb2f..d52be09705 100644 --- a/src/lib/config/gnosis-chain/index.ts +++ b/src/lib/config/gnosis-chain/index.ts @@ -8,6 +8,8 @@ import rateProviders from './rateProviders'; const config: Config = { key: '100', chainId: 100, + layerZeroChainId: 145, + supportsVeBalSync: true, chainName: 'Gnosis Chain', name: 'Gnosis Chain', shortName: 'Gnosis', diff --git a/src/lib/config/gnosis-chain/pools.ts b/src/lib/config/gnosis-chain/pools.ts index 4c8cf53813..1ff4136fc6 100644 --- a/src/lib/config/gnosis-chain/pools.ts +++ b/src/lib/config/gnosis-chain/pools.ts @@ -8,7 +8,7 @@ const pools: Pools = { PerPool: 10, PerPoolInitial: 5, }, - BoostsEnabled: false, + BoostsEnabled: true, DelegateOwner: '0xba1ba1ba1ba1ba1ba1ba1ba1ba1ba1ba1ba1ba1b', ZeroAddress: '0x0000000000000000000000000000000000000000', DynamicFees: { diff --git a/src/lib/config/goerli/contracts.ts b/src/lib/config/goerli/contracts.ts index b86deb3aad..8d25477ea8 100644 --- a/src/lib/config/goerli/contracts.ts +++ b/src/lib/config/goerli/contracts.ts @@ -23,6 +23,7 @@ const contracts: Contracts = { feeDistributorDeprecated: '0x7F91dcdE02F72b478Dc73cB21730cAcA907c8c44', faucet: '0xccb0F4Cf5D3F97f4a55bb5f5cA321C3ED033f244', gaugeRewardsHelper: goerli.ChildChainGaugeRewardHelper, + omniVotingEscrow: '0x96484f2aBF5e58b15176dbF1A799627B53F13B6d', }; export default contracts; diff --git a/src/lib/config/mainnet/contracts.ts b/src/lib/config/mainnet/contracts.ts index 6688925a41..fc0f406b82 100644 --- a/src/lib/config/mainnet/contracts.ts +++ b/src/lib/config/mainnet/contracts.ts @@ -23,6 +23,7 @@ const contracts: Contracts = { feeDistributor: mainnet.FeeDistributor, feeDistributorDeprecated: '0x26743984e3357eFC59f2fd6C1aFDC310335a61c9', faucet: '', + omniVotingEscrow: '0x96484f2aBF5e58b15176dbF1A799627B53F13B6d', }; export default contracts; diff --git a/src/lib/config/optimism/contracts.ts b/src/lib/config/optimism/contracts.ts index 0c4f0d64e4..b7f378df7f 100644 --- a/src/lib/config/optimism/contracts.ts +++ b/src/lib/config/optimism/contracts.ts @@ -17,11 +17,12 @@ const contracts: Contracts = { gaugeController: '', tokenAdmin: '', veBAL: '', - veDelegationProxy: '', + veDelegationProxy: '0x9dA18982a33FD0c7051B19F0d7C76F2d5E7e017c', veBALHelpers: '', feeDistributor: '', feeDistributorDeprecated: '', faucet: '', + gaugeWorkingBalanceHelper: optimism.ChildChainGaugeWorkingBalanceHelper, }; export default contracts; diff --git a/src/lib/config/optimism/index.ts b/src/lib/config/optimism/index.ts index bfd71c11ef..93f2ac5370 100644 --- a/src/lib/config/optimism/index.ts +++ b/src/lib/config/optimism/index.ts @@ -7,6 +7,8 @@ import tokens from './tokens'; const config: Config = { key: '10', chainId: 10, + layerZeroChainId: 111, + supportsVeBalSync: true, chainName: 'Optimism', name: 'Optimism Mainnet', shortName: 'Optimism', diff --git a/src/lib/config/optimism/pools.ts b/src/lib/config/optimism/pools.ts index 5cf4b2d7d3..78851365ce 100644 --- a/src/lib/config/optimism/pools.ts +++ b/src/lib/config/optimism/pools.ts @@ -7,7 +7,7 @@ const pools: Pools = { PerPool: 10, PerPoolInitial: 5, }, - BoostsEnabled: false, + BoostsEnabled: true, DelegateOwner: '', ZeroAddress: '', DynamicFees: { diff --git a/src/lib/config/polygon/contracts.ts b/src/lib/config/polygon/contracts.ts index 19d36c01b0..0978bb1005 100644 --- a/src/lib/config/polygon/contracts.ts +++ b/src/lib/config/polygon/contracts.ts @@ -18,12 +18,13 @@ const contracts: Contracts = { gaugeController: '', tokenAdmin: '', veBAL: '', - veDelegationProxy: '', + veDelegationProxy: '0x0f08eEf2C785AA5e7539684aF04755dEC1347b7c', veBALHelpers: '', feeDistributor: '', feeDistributorDeprecated: '', faucet: '', gaugeRewardsHelper: polygon.ChildChainGaugeRewardHelper, + gaugeWorkingBalanceHelper: polygon.ChildChainGaugeWorkingBalanceHelper, }; export default contracts; diff --git a/src/lib/config/polygon/index.ts b/src/lib/config/polygon/index.ts index 51b9bbbe82..32f166262a 100644 --- a/src/lib/config/polygon/index.ts +++ b/src/lib/config/polygon/index.ts @@ -9,6 +9,8 @@ import rateProviders from './rateProviders'; const config: Config = { key: '137', chainId: 137, + layerZeroChainId: 109, + supportsVeBalSync: true, chainName: 'Polygon PoS', name: 'Polygon Mainnet', shortName: 'Polygon', diff --git a/src/lib/config/polygon/pools.ts b/src/lib/config/polygon/pools.ts index 4d473baba1..0c419ee781 100644 --- a/src/lib/config/polygon/pools.ts +++ b/src/lib/config/polygon/pools.ts @@ -22,7 +22,7 @@ const pools: Pools = { PerPool: 10, PerPoolInitial: 5, }, - BoostsEnabled: false, + BoostsEnabled: true, DelegateOwner: '0xba1ba1ba1ba1ba1ba1ba1ba1ba1ba1ba1ba1ba1b', ZeroAddress: '0x0000000000000000000000000000000000000000', DynamicFees: { diff --git a/src/lib/config/types.ts b/src/lib/config/types.ts index a9612f48f4..d8fbe03caf 100644 --- a/src/lib/config/types.ts +++ b/src/lib/config/types.ts @@ -43,6 +43,7 @@ export interface Contracts { veBAL: string; gaugeController: string; gaugeFactory: string; + gaugeWorkingBalanceHelper?: string; balancerMinter: string; tokenAdmin: string; veDelegationProxy: string; @@ -51,6 +52,7 @@ export interface Contracts { feeDistributorDeprecated: string; faucet: string; gaugeRewardsHelper?: string; + omniVotingEscrow?: string; } export interface RateProviders { @@ -69,6 +71,7 @@ export interface Keys { export interface Config { key: string; chainId: Network; + layerZeroChainId?: number; // https://layerzero.gitbook.io/docs/technical-reference/mainnet/supported-chain-ids chainName: string; name: string; shortName: string; @@ -96,6 +99,7 @@ export interface Config { bridgeUrl: string; supportsEIP1559: boolean; supportsElementPools: boolean; + supportsVeBalSync?: boolean; blockTime: number; nativeAsset: { name: string; diff --git a/src/lib/utils/array.ts b/src/lib/utils/array.ts new file mode 100644 index 0000000000..784893e2c8 --- /dev/null +++ b/src/lib/utils/array.ts @@ -0,0 +1,3 @@ +export function allEqual(array: T[]): boolean { + return array.every(value => value === array[0]); +} diff --git a/src/lib/utils/promise.ts b/src/lib/utils/promise.ts index 38bc4b0daf..9eda56e870 100644 --- a/src/lib/utils/promise.ts +++ b/src/lib/utils/promise.ts @@ -8,7 +8,13 @@ export async function retryPromiseWithDelay( try { return await promise; } catch (e) { - if (retryCount === 1) { + const responseStatusCode = (e as any)?.response?.status || 0; + + if ( + retryCount === 1 || + responseStatusCode === 404 || + responseStatusCode === 429 + ) { return Promise.reject(e); } console.log('retrying promise', retryCount, 'time'); diff --git a/src/lib/utils/urls.ts b/src/lib/utils/urls.ts index 91aa436a82..dabc6d21ee 100644 --- a/src/lib/utils/urls.ts +++ b/src/lib/utils/urls.ts @@ -4,6 +4,7 @@ import config, { Network } from '@/lib/config'; function getNetworkIconName(network: Network) { return config[Number(network)].slug; } + export function buildNetworkIconURL(network: Network | string): string { const networkName = typeof network === 'string' ? network : getNetworkIconName(network); diff --git a/src/locales/default.json b/src/locales/default.json index f5132fc492..1e4eb5c60d 100644 --- a/src/locales/default.json +++ b/src/locales/default.json @@ -138,7 +138,7 @@ "claimHero": { "title": "Claim liquidity incentives", "legacyTitle": "Claim legacy incentives", - "description": "Balancer Protocol liquidity incentives are directed to pools by veBAL voters. Stake in these pools to earn incentives. Boost with veBAL for up to 2.5x extra on Mainnet pools.", + "description": "Balancer Protocol liquidity incentives are directed to pools by veBAL voters. Stake in these pools to earn incentives. Boost with veBAL for up to 2.5x extra.", "legacyDescription": "Liquidity mining incentive systems before the launch of ve8020-tokenomics have been deprecated. If you provided liquidity before this change and have outstanding incentives you can claim them here.", "tipLabel": { "addLiquidity": "Add liquidity", @@ -149,7 +149,7 @@ "tips": { "addLiquidity": "To earn liquidity mining incentives, add liquidity to eligible pools across Ethereum Mainnet and supported Layer 2's like Polygon and Arbitrum (see the table on the veBAL page). Note: You'll earn swap fees with or without staking but you don't need to claim these as they are automatically added to your position.", "stake": "If you've provided liquidity in an eligible pool, you'll have an option to stake your Liquidity Provider (LP) tokens from that pool. Stake them to receive liquidity mining incentives in addition to any swap fees.", - "boost": "Get veBAL to boost your liquidity mining incentives by up to 2.5x (on Ethereum Mainnet only). The more veBAL you hold, the larger your boost. veBAL holders also earn protocol fees and can vote to direct future pool emissions. Note, there is no additional boost from holding veBAL on Layer 2 pools like Polygon and Arbitrum.", + "boost": "Get veBAL to boost your liquidity mining incentives by up to 2.5x, including on L2 networks via cross-chain boosts. The more veBAL you hold, the larger your boost. veBAL holders also earn protocol fees and can vote to direct future pool emissions.", "claim": "Token earnings from liquidity mining accumulate every block and can be claimed at any time. Some pools offer multiple token incentives (not just BAL). Additionally, veBAL holders also get a share of protocol revenue, in both bb-a-USD and BAL." } }, @@ -235,6 +235,51 @@ "zeroWeightTitle": "You’ve included a token with zero weight", "zeroWeightInfo": "All tokens in a pool must have a weighting greater than zero. Either remove or replace {0} or set it above 0.01%." }, + "crossChainBoost": { + "title": "Cross chain veBAL boosts", + "infoDescription": "Sidechains & Layer 2 networks like Polygon and Arbitrum don’t know your veBAL balance on Ethereum Mainnet, unless you sync it. On any network where you stake, you should sync your veBAL balance to get your max possible boost. Resync after acquiring more veBAL to continue boosting to your max.", + "sync": "Sync", + "currentBalance": "Current balance", + "postSyncBalance": "Post-sync balance", + "syncedNetworks": "Synced networks", + "syncToNetwork": "Sync veBAL to {network}", + "syncingToNetwork": "Syncing veBAL to {network}", + "unsyncedNetworks": "Unsynced networks", + "unsyncedAllDescription": "Sync veBAL across networks for a boosted APR on your staked positions.", + "syncedAllDescription": "All networks are synced", + "emptyState": "Once you have some veBAL, sync your balance here to other networks.", + "syncProcessWarning": { + "title": "Wait until sync finalizes before restaking / triggering a gauge update on L2", + "description": "Your sync has been initiated but it may take up to 30 mins to update across L2s. Once your veBAL is synced, you will need to interact with each gauge to register your new max boost. You can either claim, restake, or click the Update button, which will appear on each individual pool page staking section." + }, + "syncComplete": { + "title": "Remember to restake to the new boost-aware L2 pool gauges", + "description": "To get boosted yield on L2 networks, go to your Portfolio page on the L2 network and restake from the deprecated pool gauges to the new boost-aware pool gauges." + }, + "updateGauge": { + "title": "Trigger pool gauge updates to get your boosts sooner", + "description": "Pool gauges don’t automatically recognize changes in veBAL until triggered. Updates are triggered when any user interacts with a gauge, such as by claiming BAL, staking or unstaking. Trigger individual gauges yourself for your boosts to apply immediately." + }, + "selectNetworkModal": { + "title": "Sync veBAL: Select networks", + "description": "Layer 2 networks don’t know your veBAL balance from Ethereum, unless you sync it. Each network costs additional gas to sync, so it’s best to only sync networks where you plan to stake." + }, + "syncNetworkModal": { + "title": "Sync veBAL", + "singleNetworkTitle": "Sync veBAL to {0}", + "description": "This will sync your veBAL balance and give you a staking boost on {0}." + }, + "syncInitiatedModal": { + "title": "veBAL sync initiated", + "description": "Your veBAL balance is now being synced to the following networks:", + "warningTitle": "Your L2 veBAL sync is in progress", + "warningDescription": "Your sync has been initiated but it may take up to 30 mins to update across L2s. Please check the your portfolio on the destination chain/s after this time, there will be actions available to update boost on your positions. You can also update all your positions by claiming rewards on the synced chain." + }, + "syncNetworkAction": { + "title": "This will sync your veBAL balance and give you a staking boost across the networks listed below." + }, + "syncInProgress": "A sync operation is currently in progress. Wait until it completes before restaking any of your positions on {network} to get your maximum veBAL boost." + }, "migratePool": { "aaveBoostedPool": { "whyMigrate": { @@ -1447,7 +1492,9 @@ "createLock": "Lock", "extendLock": "Extend lock", "increaseLock": "Increase lock", - "unlock": "Unlock" + "unlock": "Unlock", + "sync": "Sync", + "userGaugeCheckpoint": "Pool gauge veBAL update" }, "transactionDeadline": "Transaction deadline", "transactionDeadlineTooltip": "Your swap will expire and not execute if it is pending for more than the selected duration. Only executed transactions incur fees for swaps between ERC-20 tokens.", diff --git a/src/pages/_layouts/DefaultLayout.vue b/src/pages/_layouts/DefaultLayout.vue index cce554af42..a6d70ee57b 100644 --- a/src/pages/_layouts/DefaultLayout.vue +++ b/src/pages/_layouts/DefaultLayout.vue @@ -1,32 +1,13 @@ - - diff --git a/src/pages/_layouts/JoinExitLayout.vue b/src/pages/_layouts/PoolLayout.vue similarity index 66% rename from src/pages/_layouts/JoinExitLayout.vue rename to src/pages/_layouts/PoolLayout.vue index 7847cc0976..f5791c1ce3 100644 --- a/src/pages/_layouts/JoinExitLayout.vue +++ b/src/pages/_layouts/PoolLayout.vue @@ -1,5 +1,6 @@ diff --git a/src/pages/pool/_id.vue b/src/pages/pool/_id.vue index 41cfeff555..eb9b094c53 100644 --- a/src/pages/pool/_id.vue +++ b/src/pages/pool/_id.vue @@ -1,7 +1,5 @@