From d8185ce88fe592d24affe0f45054aa363424319f Mon Sep 17 00:00:00 2001 From: Tim Robinson Date: Tue, 4 Jul 2023 18:20:42 +1000 Subject: [PATCH 01/16] Ingore user errors in more places --- .../LockPreviewModal/components/LockActions.vue | 17 ++++++++++------- src/providers/local/join-pool.provider.ts | 3 +++ 2 files changed, 13 insertions(+), 7 deletions(-) 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..ab59e9ca60 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 { isUserRejected } 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 (!isUserRejected(error)) { + captureException(error, { + level: 'fatal', + extra: { + lockType, + props, + }, + }); + } return Promise.reject(error); } diff --git a/src/providers/local/join-pool.provider.ts b/src/providers/local/join-pool.provider.ts index 1f90305547..f28b2cd2e6 100644 --- a/src/providers/local/join-pool.provider.ts +++ b/src/providers/local/join-pool.provider.ts @@ -49,6 +49,7 @@ import useTokenApprovalActions from '@/composables/approvals/useTokenApprovalAct import { useApp } from '@/composables/useApp'; import { throwQueryError } from '@/lib/utils/queries'; import { ApprovalAction } from '@/composables/approvals/types'; +import { isUserRejected } from '@/composables/useTransactionErrors'; /** * TYPES @@ -375,6 +376,8 @@ export const joinPoolProvider = ( } async function logJoinException(error: Error) { + if (isUserRejected(error)) return; + const sender = await getSigner().getAddress(); captureException(error, { level: 'fatal', From b97c866b2fda2d27f620b2214dd53c2a6834405d Mon Sep 17 00:00:00 2001 From: Gareth Fuller Date: Tue, 4 Jul 2023 14:03:24 +0100 Subject: [PATCH 02/16] fix: Make sure deltas returned from SDK have a value (#3622) --- src/composables/swap/useSor.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/composables/swap/useSor.ts b/src/composables/swap/useSor.ts index 8d818eca52..f6d478d477 100644 --- a/src/composables/swap/useSor.ts +++ b/src/composables/swap/useSor.ts @@ -308,10 +308,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 +324,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, From 323bc074f769af03028cd9f57cb7e888797f7f49 Mon Sep 17 00:00:00 2001 From: Tim Robinson Date: Wed, 5 Jul 2023 00:29:51 +1000 Subject: [PATCH 03/16] More ignoring of user errors and improvements to error handling (#3621) * Fix naming issues * Include another user cancellation error in isUserRejected check * Switch errors to regex to match part of errors instead of exact matching the entire thing * Add comment about out of gas error * Add test and message for another user disapproval --- .../contextual/pages/claim/LegacyClaims.vue | 4 +- src/composables/swap/useCowswap.ts | 4 +- src/composables/swap/useJoinExit.ts | 4 +- src/composables/swap/useSor.ts | 4 +- ...s.spec.ts => useTransactionErrors.spec.ts} | 27 +++++++++++- src/composables/useTransactionErrors.ts | 41 +++++++++++-------- 6 files changed, 59 insertions(+), 25 deletions(-) rename src/composables/{userTransactionErrors.spec.ts => useTransactionErrors.spec.ts} (58%) 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/composables/swap/useCowswap.ts b/src/composables/swap/useCowswap.ts index 1b24748222..f81a7ce9ec 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 useTransactionErrors from '../useTransactionErrors'; import { useI18n } from 'vue-i18n'; import { useApp } from '@/composables/useApp'; @@ -93,7 +93,7 @@ export default function useCowswap({ const { addTransaction } = useTransactions(); const { fNum } = useNumbers(); const { balanceFor } = useTokens(); - const { isUserRejected } = useTranasactionErrors(); + const { isUserRejected } = useTransactionErrors(); const { t } = useI18n(); // DATA diff --git a/src/composables/swap/useJoinExit.ts b/src/composables/swap/useJoinExit.ts index 8e1f8b9668..d1d43488c8 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 useTransactionErrors from '../useTransactionErrors'; import { useI18n } from 'vue-i18n'; type JoinExitState = { @@ -91,7 +91,7 @@ export default function useJoinExit({ const { addTransaction } = useTransactions(); const { txListener } = useEthers(); const { fNum } = useNumbers(); - const { isUserRejected } = useTranasactionErrors(); + const { isUserRejected } = useTransactionErrors(); const { t } = useI18n(); const hasValidationError = computed( diff --git a/src/composables/swap/useSor.ts b/src/composables/swap/useSor.ts index f6d478d477..7feb6dbbf0 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 useTransactionErrors from '../useTransactionErrors'; type SorState = { validationErrors: { @@ -204,7 +204,7 @@ export default function useSor({ const { fNum, toFiat } = useNumbers(); const { t } = useI18n(); const { injectTokens, priceFor, getToken } = useTokens(); - const { isUserRejected } = useTranasactionErrors(); + const { isUserRejected } = useTransactionErrors(); const { swapIn, swapOut } = useSwapper(); onMounted(async () => { 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..893c0c262c 100644 --- a/src/composables/useTransactionErrors.ts +++ b/src/composables/useTransactionErrors.ts @@ -6,45 +6,54 @@ 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', + /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/, ]; if ( typeof error === 'string' && - userRejectionMessages.includes(error.toLowerCase()) + userRejectionMessages.some(msg => msg.test(error.toLowerCase())) ) return true; if ( error.message && - userRejectionMessages.includes(error.message.toLowerCase()) + userRejectionMessages.some(msg => msg.test(error.message.toLowerCase())) ) return true; if ( typeof error.reason === 'string' && - userRejectionMessages.includes(error.reason.toLowerCase()) + userRejectionMessages.some(msg => msg.test(error.reason.toLowerCase())) ) return true; if ( error.cause?.message && - userRejectionMessages.includes(error.cause.message.toLowerCase()) + userRejectionMessages.some(msg => + msg.test(error.cause.message.toLowerCase()) + ) ) return true; if ( typeof error.cause === 'string' && - userRejectionMessages.includes(error.cause.toLowerCase()) + userRejectionMessages.some(msg => msg.test(error.cause.toLowerCase())) + ) + return true; + + if ( + error.b && + userRejectionMessages.some(msg => msg.test(error.b.toLowerCase())) ) return true; @@ -57,7 +66,7 @@ export function isUserRejected(error): boolean { return false; } -export default function useTranasactionErrors() { +export default function useTransactionErrors() { /** * COMPOSABLES */ From d025edfe82268322d59fec50070f03b08be2d7f0 Mon Sep 17 00:00:00 2001 From: Daniel <91405705+danielmkm@users.noreply.github.com> Date: Wed, 5 Jul 2023 16:07:24 +0800 Subject: [PATCH 04/16] chore: add a filter for insufficient funds (#3627) --- src/composables/useTransactionErrors.ts | 34 +++++++++++++++---------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/composables/useTransactionErrors.ts b/src/composables/useTransactionErrors.ts index 893c0c262c..f7fd128aeb 100644 --- a/src/composables/useTransactionErrors.ts +++ b/src/composables/useTransactionErrors.ts @@ -3,9 +3,7 @@ import { useI18n } from 'vue-i18n'; import { TransactionError } from '@/types/transactions'; export function isUserRejected(error): boolean { - if (!error) return false; - - const userRejectionMessages = [ + const messages = [ /user rejected transaction/, /request rejected/, /user rejected methods./, @@ -19,42 +17,49 @@ export function isUserRejected(error): boolean { /user disapproved requested methods/, ]; + 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.some(msg => msg.test(error.toLowerCase())) + messages.some(msg => msg.test(error.toLowerCase())) ) return true; if ( error.message && - userRejectionMessages.some(msg => msg.test(error.message.toLowerCase())) + messages.some(msg => msg.test(error.message.toLowerCase())) ) return true; if ( typeof error.reason === 'string' && - userRejectionMessages.some(msg => msg.test(error.reason.toLowerCase())) + messages.some(msg => msg.test(error.reason.toLowerCase())) ) return true; if ( error.cause?.message && - userRejectionMessages.some(msg => - msg.test(error.cause.message.toLowerCase()) - ) + messages.some(msg => msg.test(error.cause.message.toLowerCase())) ) return true; if ( typeof error.cause === 'string' && - userRejectionMessages.some(msg => msg.test(error.cause.toLowerCase())) + messages.some(msg => msg.test(error.cause.toLowerCase())) ) return true; - if ( - error.b && - userRejectionMessages.some(msg => msg.test(error.b.toLowerCase())) - ) + if (error.b && messages.some(msg => msg.test(error.b.toLowerCase()))) return true; if (error?.code && error.code === 4001) { @@ -102,6 +107,7 @@ export default function useTransactionErrors() { */ function parseError(error): TransactionError | null { if (isUserRejected(error)) return null; // User rejected transaction + if (isUserNotEnoughGas(error)) return null; // User does not have enough gas if (error?.code && error.code === 'UNPREDICTABLE_GAS_LIMIT') return cannotEstimateGasError; From 4932e79efe0b6a602e0ff7f9b421b88acb0df68d Mon Sep 17 00:00:00 2001 From: Daniel <91405705+danielmkm@users.noreply.github.com> Date: Wed, 5 Jul 2023 16:09:50 +0800 Subject: [PATCH 05/16] Fix/exit retry loop when 404 or 429 (#3626) * Exit the retry loop when you get 404 or 429 status codes * Return an empty object to prevent multiple failed attempts --- .../queries/useHistoricalPricesQuery.ts | 21 +++++++++++++------ src/lib/utils/promise.ts | 8 ++++++- 2 files changed, 22 insertions(+), 7 deletions(-) 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/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'); From 649784edbb8571eb5850aab7145881b3b95d6dff Mon Sep 17 00:00:00 2001 From: Gareth Fuller Date: Wed, 5 Jul 2023 11:16:20 +0100 Subject: [PATCH 06/16] feat: re-enable native asset joins for deep pools (#3624) * feat: Re-enable native asset joins for deep pools * chore: Log query inputs * chore: Ensure zero addrress is passed for native asset joins * chore: Pass value to sendTransaction * chore: Fix test --- .../AddLiquidityForm/AddLiquidityForm.vue | 2 +- .../handlers/generalised-join.handler.spec.ts | 2 ++ .../handlers/generalised-join.handler.ts | 26 ++++++++++++++++--- tests/unit/builders/signer.ts | 1 + 4 files changed, 26 insertions(+), 5 deletions(-) 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/services/balancer/pools/joins/handlers/generalised-join.handler.spec.ts b/src/services/balancer/pools/joins/handlers/generalised-join.handler.spec.ts index e24d938514..8d37dfe0e6 100644 --- a/src/services/balancer/pools/joins/handlers/generalised-join.handler.spec.ts +++ b/src/services/balancer/pools/joins/handlers/generalised-join.handler.spec.ts @@ -9,6 +9,7 @@ import { buildJoinParams } from '@tests/unit/builders/join-exit.builders'; import { defaultGasLimit, defaultTransactionResponse, + defaultTxValue, } from '@tests/unit/builders/signer'; import { GeneralisedJoinHandler } from './generalised-join.handler'; import { initContractConcernWithDefaultMocks } from '@/dependencies/contract.concern.mocks'; @@ -31,5 +32,6 @@ test('Successfully executes a generalized join transaction', async () => { data: defaultGeneralizedJoinResponse.encodedCall, to: defaultGeneralizedJoinResponse.to, gasLimit: defaultGasLimit, + value: defaultTxValue, }); }); diff --git a/src/services/balancer/pools/joins/handlers/generalised-join.handler.ts b/src/services/balancer/pools/joins/handlers/generalised-join.handler.ts index b29fc55818..3495e9dc77 100644 --- a/src/services/balancer/pools/joins/handlers/generalised-join.handler.ts +++ b/src/services/balancer/pools/joins/handlers/generalised-join.handler.ts @@ -4,8 +4,10 @@ import { TransactionResponse } from '@ethersproject/abstract-provider'; import { Ref } from 'vue'; import { JoinParams, JoinPoolHandler, QueryOutput } from './join-pool.handler'; import { formatFixed, parseFixed } from '@ethersproject/bignumber'; -import { bnum, selectByAddress } from '@/lib/utils'; +import { bnum, isSameAddress, selectByAddress } from '@/lib/utils'; import { TransactionBuilder } from '@/services/web3/transactions/transaction.builder'; +import { configService } from '@/services/config/config.service'; +import { AddressZero } from '@ethersproject/constants'; type JoinResponse = Awaited< ReturnType @@ -30,9 +32,9 @@ export class GeneralisedJoinHandler implements JoinPoolHandler { } const txBuilder = new TransactionBuilder(params.signer); - const { to, encodedCall } = this.lastJoinRes; + const { to, encodedCall, value } = this.lastJoinRes; - return txBuilder.raw.sendTransaction({ to, data: encodedCall }); + return txBuilder.raw.sendTransaction({ to, data: encodedCall, value }); } async queryJoin({ @@ -52,7 +54,9 @@ export class GeneralisedJoinHandler implements JoinPoolHandler { return parseFixed(value || '0', token.decimals).toString(); }); - const tokenAddresses: string[] = amountsIn.map(({ address }) => address); + const tokenAddresses: string[] = amountsIn.map(({ address }) => + this.formatTokenAddress(address) + ); const signerAddress = await signer.getAddress(); const slippage = slippageBsp.toString(); const poolId = this.pool.value.id; @@ -96,4 +100,18 @@ export class GeneralisedJoinHandler implements JoinPoolHandler { priceImpact, }; } + + /** + * If native asset addres, replaces with zero address because the vault only checks + * for the zero address when joining with native asset. + */ + private formatTokenAddress(address: string): string { + const { nativeAsset } = configService.network.tokens.Addresses; + + if (isSameAddress(address, nativeAsset)) { + return AddressZero; + } + + return address; + } } diff --git a/tests/unit/builders/signer.ts b/tests/unit/builders/signer.ts index 52b6bda90e..192f2ac5f7 100644 --- a/tests/unit/builders/signer.ts +++ b/tests/unit/builders/signer.ts @@ -10,6 +10,7 @@ defaultTransactionResponse.data = 'default data'; export const defaultGasLimit = 2; const defaultEstimatedGas = BigNumber.from(defaultGasLimit); +export const defaultTxValue = BigNumber.from(0); export function aSigner(...options: Partial[]): JsonRpcSigner { const defaultSigner = mock(); From 389149f831f972c2c5325e308ad04f71cab10764 Mon Sep 17 00:00:00 2001 From: Gareth Fuller Date: Wed, 5 Jul 2023 11:16:42 +0100 Subject: [PATCH 07/16] refactor: Catch all user triggered errors (#3628) --- .../LockPreviewModal/components/LockActions.vue | 4 ++-- src/composables/swap/useCowswap.ts | 5 ++--- src/composables/swap/useJoinExit.ts | 5 ++--- src/composables/swap/useSor.ts | 5 ++--- src/composables/useTransactionErrors.ts | 8 ++++++-- src/providers/local/join-pool.provider.ts | 4 ++-- .../web3/transactions/concerns/transaction.concern.ts | 4 ++-- 7 files changed, 18 insertions(+), 17 deletions(-) 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 ab59e9ca60..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,7 +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 { isUserRejected } from '@/composables/useTransactionErrors'; +import { isUserError } from '@/composables/useTransactionErrors'; /** * TYPES @@ -188,7 +188,7 @@ 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 - if (!isUserRejected(error)) { + if (!isUserError(error)) { captureException(error, { level: 'fatal', extra: { diff --git a/src/composables/swap/useCowswap.ts b/src/composables/swap/useCowswap.ts index f81a7ce9ec..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 useTransactionErrors 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 } = useTransactionErrors(); 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 d1d43488c8..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 useTransactionErrors 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 } = useTransactionErrors(); 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 7feb6dbbf0..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 useTransactionErrors 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 } = useTransactionErrors(); const { swapIn, swapOut } = useSwapper(); onMounted(async () => { @@ -799,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/useTransactionErrors.ts b/src/composables/useTransactionErrors.ts index f7fd128aeb..ac28f33d16 100644 --- a/src/composables/useTransactionErrors.ts +++ b/src/composables/useTransactionErrors.ts @@ -71,6 +71,11 @@ function isErrorType(error, messages: RegExp[]): boolean { return false; } +// 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 @@ -106,8 +111,7 @@ export default function useTransactionErrors() { * METHODS */ function parseError(error): TransactionError | null { - if (isUserRejected(error)) return null; // User rejected transaction - if (isUserNotEnoughGas(error)) return null; // User does not have enough gas + if (isUserError(error)) return null; if (error?.code && error.code === 'UNPREDICTABLE_GAS_LIMIT') return cannotEstimateGasError; diff --git a/src/providers/local/join-pool.provider.ts b/src/providers/local/join-pool.provider.ts index f28b2cd2e6..9efe766528 100644 --- a/src/providers/local/join-pool.provider.ts +++ b/src/providers/local/join-pool.provider.ts @@ -49,7 +49,7 @@ import useTokenApprovalActions from '@/composables/approvals/useTokenApprovalAct import { useApp } from '@/composables/useApp'; import { throwQueryError } from '@/lib/utils/queries'; import { ApprovalAction } from '@/composables/approvals/types'; -import { isUserRejected } from '@/composables/useTransactionErrors'; +import { isUserError } from '@/composables/useTransactionErrors'; /** * TYPES @@ -376,7 +376,7 @@ export const joinPoolProvider = ( } async function logJoinException(error: Error) { - if (isUserRejected(error)) return; + if (isUserError(error)) return; const sender = await getSigner().getAddress(); captureException(error, { diff --git a/src/services/web3/transactions/concerns/transaction.concern.ts b/src/services/web3/transactions/concerns/transaction.concern.ts index 65240c82a1..c13fd8d82d 100644 --- a/src/services/web3/transactions/concerns/transaction.concern.ts +++ b/src/services/web3/transactions/concerns/transaction.concern.ts @@ -1,4 +1,4 @@ -import { isUserRejected } from '@/composables/useTransactionErrors'; +import { isUserError } from '@/composables/useTransactionErrors'; import { configService } from '@/services/config/config.service'; import { gasService } from '@/services/gas/gas.service'; import { rpcProviderService } from '@/services/rpc-provider/rpc-provider.service'; @@ -11,6 +11,6 @@ export class TransactionConcern { ) {} public shouldLogFailure(error): boolean { - return this.config.env.APP_ENV !== 'development' && !isUserRejected(error); + return this.config.env.APP_ENV !== 'development' && !isUserError(error); } } From 18ec2d2a27b5903ad53d302f01975ce03513d412 Mon Sep 17 00:00:00 2001 From: Gareth Fuller Date: Wed, 5 Jul 2023 11:29:11 +0100 Subject: [PATCH 08/16] chore: Remove zkevm promo (DM) (#3630) * 1.110.1 * weekly gauge add * 1.110.2 * Remove sentry log when price is not found for a token, as it's very noisy * Refetch allowances before getting required approvals again * 1.110.3 * chore: Remove promo banner --------- Co-authored-by: Tim Robinson Co-authored-by: Automated Version Bump Co-authored-by: ZeKraken <79888567+zekraken-bot@users.noreply.github.com> Co-authored-by: Tim Robinson --- package-lock.json | 4 +- package.json | 2 +- public/data/hardcoded-gauges.json | 29 ++ public/images/backgrounds/zkevm-promo-bg.jpg | Bin 62115 -> 0 bytes .../contextual/pages/pools/ZkevmPromo.vue | 56 ---- .../approvals/useTokenApprovalActions.ts | 1 + src/data/voting-gauges.json | 279 ++++++++++++------ src/lib/config/optimism/pools.ts | 5 + src/pages/index.vue | 2 - src/providers/tokens.provider.ts | 6 - 10 files changed, 227 insertions(+), 157 deletions(-) delete mode 100644 public/images/backgrounds/zkevm-promo-bg.jpg delete mode 100644 src/components/contextual/pages/pools/ZkevmPromo.vue diff --git a/package-lock.json b/package-lock.json index b71b92fdf9..a734f52f0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@balancer/frontend-v2", - "version": "1.110.0", + "version": "1.110.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@balancer/frontend-v2", - "version": "1.110.0", + "version": "1.110.3", "license": "MIT", "devDependencies": { "@aave/protocol-js": "^4.3.0", diff --git a/package.json b/package.json index 3e729cbe77..4c6bdaf28b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@balancer/frontend-v2", - "version": "1.110.0", + "version": "1.110.3", "engines": { "node": "=16", "npm": ">=8" diff --git a/public/data/hardcoded-gauges.json b/public/data/hardcoded-gauges.json index dfefaa42fe..0e5de6ebab 100644 --- a/public/data/hardcoded-gauges.json +++ b/public/data/hardcoded-gauges.json @@ -85,5 +85,34 @@ "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270.png", "0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4.png" } + }, + { + "address": "0x730A168cF6F501cf302b803FFc57FF3040f378Bf", + "network": 10, + "isKilled": false, + "addedTimestamp": 1688418508, + "relativeWeightCap": "0.10", + "pool": { + "id": "0x7ca75bdea9dede97f8b13c6641b768650cb837820002000000000000000000d5", + "address": "0x7Ca75bdEa9dEde97F8B13C6641B768650CB83782", + "poolType": "ComposableStable", + "symbol": "ECLP-wstETH-WETH", + "tokens": [ + { + "address": "0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb", + "weight": "null", + "symbol": "wstETH" + }, + { + "address": "0x4200000000000000000000000000000000000006", + "weight": "null", + "symbol": "WETH" + } + ] + }, + "tokenLogoURIs": { + "0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0x1f32b1c2345538c0c6f582fcb022739c4a194ebb.png", + "0x4200000000000000000000000000000000000006": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0x4200000000000000000000000000000000000006.png" + } } ] diff --git a/public/images/backgrounds/zkevm-promo-bg.jpg b/public/images/backgrounds/zkevm-promo-bg.jpg deleted file mode 100644 index 4716e4beb7d3fe2327392e81b89126c973566f12..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 62115 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

DEj>f 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%0A& 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 diff --git a/src/components/contextual/pages/pools/ZkevmPromo.vue b/src/components/contextual/pages/pools/ZkevmPromo.vue deleted file mode 100644 index 6ae85e4154..0000000000 --- a/src/components/contextual/pages/pools/ZkevmPromo.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - - - diff --git a/src/composables/approvals/useTokenApprovalActions.ts b/src/composables/approvals/useTokenApprovalActions.ts index e43609610f..c40b29cc6b 100644 --- a/src/composables/approvals/useTokenApprovalActions.ts +++ b/src/composables/approvals/useTokenApprovalActions.ts @@ -95,6 +95,7 @@ export default function useTokenApprovalActions() { spender: string ): Promise { await injectSpenders([spender]); + await refetchAllowances(); return approvalsRequired(amountsToApprove, spender); } diff --git a/src/data/voting-gauges.json b/src/data/voting-gauges.json index 20c4979b79..75a2747076 100644 --- a/src/data/voting-gauges.json +++ b/src/data/voting-gauges.json @@ -175,6 +175,35 @@ "0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4.png" } }, + { + "address": "0x730A168cF6F501cf302b803FFc57FF3040f378Bf", + "network": 10, + "isKilled": false, + "addedTimestamp": 1688418508, + "relativeWeightCap": "0.10", + "pool": { + "id": "0x7ca75bdea9dede97f8b13c6641b768650cb837820002000000000000000000d5", + "address": "0x7Ca75bdEa9dEde97F8B13C6641B768650CB83782", + "poolType": "ComposableStable", + "symbol": "ECLP-wstETH-WETH", + "tokens": [ + { + "address": "0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb", + "weight": "null", + "symbol": "wstETH" + }, + { + "address": "0x4200000000000000000000000000000000000006", + "weight": "null", + "symbol": "WETH" + } + ] + }, + "tokenLogoURIs": { + "0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0x1f32b1c2345538c0c6f582fcb022739c4a194ebb.png", + "0x4200000000000000000000000000000000000006": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0x4200000000000000000000000000000000000006.png" + } + }, { "address": "0x34f33CDaED8ba0E1CEECE80e5f4a73bcf234cfac", "network": 1, @@ -560,7 +589,7 @@ ] }, "tokenLogoURIs": { - "0x81f8f0bb1cB2A06649E51913A151F0E7Ef6FA321": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0x81f8f0bb1cb2a06649e51913a151f0e7ef6fa321.png", + "0x81f8f0bb1cB2A06649E51913A151F0E7Ef6FA321": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x81f8f0bb1cB2A06649E51913A151F0E7Ef6FA321/logo.png", "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.png" } }, @@ -763,7 +792,7 @@ ] }, "tokenLogoURIs": { - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.png", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", "0xEd1480d12bE41d92F36f5f7bDd88212E381A3677": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0xed1480d12be41d92f36f5f7bdd88212e381a3677.png" } }, @@ -883,35 +912,6 @@ "0xf203Ca1769ca8e9e8FE1DA9D147DB68B6c919817": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0xf203ca1769ca8e9e8fe1da9d147db68b6c919817.png" } }, - { - "address": "0x9F65d476DD77E24445A48b4FeCdeA81afAA63480", - "network": 1, - "isKilled": true, - "relativeWeightCap": "null", - "addedTimestamp": 1653599177, - "pool": { - "id": "0x85370d9e3bb111391cc89f6de344e801760461830002000000000000000001ef", - "address": "0x85370D9e3bb111391cc89F6DE344E80176046183", - "poolType": "Weighted", - "symbol": "CREAM_ETH", - "tokens": [ - { - "address": "0x2ba592F78dB6436527729929AAf6c908497cB200", - "weight": "0.8", - "symbol": "CREAM" - }, - { - "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - "weight": "0.2", - "symbol": "WETH" - } - ] - }, - "tokenLogoURIs": { - "0x2ba592F78dB6436527729929AAf6c908497cB200": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x2ba592F78dB6436527729929AAf6c908497cB200/logo.png", - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.png" - } - }, { "address": "0xe2b680A8d02fbf48C7D9465398C4225d7b7A7f87", "network": 1, @@ -3142,35 +3142,6 @@ "0xfeBb0bbf162E64fb9D0dfe186E517d84C395f016": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0xfebb0bbf162e64fb9d0dfe186e517d84c395f016.png" } }, - { - "address": "0xc43bF12A008d3Cc48AF7da1e8e87622A78dc64da", - "network": 1, - "isKilled": false, - "relativeWeightCap": "1", - "addedTimestamp": 1684791827, - "pool": { - "id": "0xeb567dde03f3da7fe185bdacd5ab495ab220769d000000000000000000000548", - "address": "0xEb567DDE03F3DA7FE185BDaCD5AB495AB220769d", - "poolType": "ComposableStable", - "symbol": "ankrETH-bb-a-WETH-BPT", - "tokens": [ - { - "address": "0x60D604890feaa0b5460B28A424407c24fe89374a", - "weight": "null", - "symbol": "bb-a-WETH" - }, - { - "address": "0xE95A203B1a91a908F9B9CE46459d101078c2c3cb", - "weight": "null", - "symbol": "ankrETH" - } - ] - }, - "tokenLogoURIs": { - "0x60D604890feaa0b5460B28A424407c24fe89374a": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0x60d604890feaa0b5460b28a424407c24fe89374a.png", - "0xE95A203B1a91a908F9B9CE46459d101078c2c3cb": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0xe95a203b1a91a908f9b9ce46459d101078c2c3cb.png" - } - }, { "address": "0x69F1077AeCE23D5b0344330B5eB13f05d5e410a1", "network": 1, @@ -3606,6 +3577,163 @@ "0x9Bcef72be871e61ED4fBbc7630889beE758eb81D": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/optimism/assets/0x9Bcef72be871e61ED4fBbc7630889beE758eb81D/logo.png" } }, + { + "address": "0x1Ce5bf7e6C16C567DeFd625e0911Bfd0FC7f2d7d", + "network": 10, + "isKilled": false, + "relativeWeightCap": "1", + "addedTimestamp": 1688412059, + "pool": { + "id": "0x39965c9dab5448482cf7e002f583c812ceb53046000100000000000000000003", + "address": "0x39965c9dAb5448482Cf7e002F583c812Ceb53046", + "poolType": "Weighted", + "symbol": "BPT-ROAD", + "tokens": [ + { + "address": "0x4200000000000000000000000000000000000006", + "weight": "0.4", + "symbol": "WETH" + }, + { + "address": "0x4200000000000000000000000000000000000042", + "weight": "0.4", + "symbol": "OP" + }, + { + "address": "0x7F5c764cBc14f9669B88837ca1490cCa17c31607", + "weight": "0.2", + "symbol": "USDC" + } + ] + }, + "tokenLogoURIs": { + "0x4200000000000000000000000000000000000006": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/optimism/assets/0x4200000000000000000000000000000000000006/logo.png", + "0x4200000000000000000000000000000000000042": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/optimism/assets/0x4200000000000000000000000000000000000042/logo.png", + "0x7F5c764cBc14f9669B88837ca1490cCa17c31607": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/optimism/assets/0x7F5c764cBc14f9669B88837ca1490cCa17c31607/logo.png" + } + }, + { + "address": "0xd0b6787589d33B4F7aA5a27f36497e091e78a2ad", + "network": 10, + "isKilled": false, + "relativeWeightCap": "0.02", + "addedTimestamp": 1688412059, + "pool": { + "id": "0x1d95129c18a8c91c464111fdf7d0eb241b37a9850002000000000000000000c1", + "address": "0x1D95129c18a8c91C464111FDf7d0Eb241B37a985", + "poolType": "Weighted", + "symbol": "BPT-RESERVE", + "tokens": [ + { + "address": "0x7F5c764cBc14f9669B88837ca1490cCa17c31607", + "weight": "0.5", + "symbol": "USDC" + }, + { + "address": "0xc5b001DC33727F8F26880B184090D3E252470D45", + "weight": "0.5", + "symbol": "ERN" + } + ] + }, + "tokenLogoURIs": { + "0x7F5c764cBc14f9669B88837ca1490cCa17c31607": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/optimism/assets/0x7F5c764cBc14f9669B88837ca1490cCa17c31607/logo.png", + "0xc5b001DC33727F8F26880B184090D3E252470D45": "" + } + }, + { + "address": "0x0BFcF593C149Ddbeedb190667d24D30D2E38AF73", + "network": 10, + "isKilled": false, + "relativeWeightCap": "0.02", + "addedTimestamp": 1688412059, + "pool": { + "id": "0xd20f6f1d8a675cdca155cb07b5dc9042c467153f0002000000000000000000bc", + "address": "0xd20f6F1D8a675cDCa155Cb07b5dC9042c467153f", + "poolType": "Weighted", + "symbol": "BPT-BOATH", + "tokens": [ + { + "address": "0x39FdE572a18448F8139b7788099F0a0740f51205", + "weight": "0.8", + "symbol": "OATH" + }, + { + "address": "0x4200000000000000000000000000000000000006", + "weight": "0.2", + "symbol": "WETH" + } + ] + }, + "tokenLogoURIs": { + "0x39FdE572a18448F8139b7788099F0a0740f51205": "", + "0x4200000000000000000000000000000000000006": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/optimism/assets/0x4200000000000000000000000000000000000006/logo.png" + } + }, + { + "address": "0xdc08146530DD9910F8ab4D0aD2C184f87e903540", + "network": 10, + "isKilled": false, + "relativeWeightCap": "1", + "addedTimestamp": 1688412059, + "pool": { + "id": "0x098f32d98d0d64dba199fc1923d3bf4192e787190001000000000000000000d2", + "address": "0x098f32D98d0D64Dba199FC1923D3BF4192E78719", + "poolType": "Weighted", + "symbol": "bb-rf-SOTRI", + "tokens": [ + { + "address": "0x6af3737F6d58Ae8Bcb9f2B597125D37244596E59", + "weight": "0.25", + "symbol": "bb-rf-soWBTC" + }, + { + "address": "0x7e9250cC13559eB50536859e8C076Ef53e275Fb3", + "weight": "0.5", + "symbol": "bb-rf-soWSTETH" + }, + { + "address": "0xEdcfaF390906a8f91fb35B7bAC23f3111dBaEe1C", + "weight": "0.25", + "symbol": "bb-rf-soUSDC" + } + ] + }, + "tokenLogoURIs": { + "0x6af3737F6d58Ae8Bcb9f2B597125D37244596E59": "", + "0x7e9250cC13559eB50536859e8C076Ef53e275Fb3": "", + "0xEdcfaF390906a8f91fb35B7bAC23f3111dBaEe1C": "" + } + }, + { + "address": "0x1b8C2C972c67f4A5B43C2EbE07E64fCB88ACee87", + "network": 10, + "isKilled": false, + "relativeWeightCap": "1", + "addedTimestamp": 1688412059, + "pool": { + "id": "0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb200020000000000000000008b", + "address": "0x7B50775383d3D6f0215A8F290f2C9e2eEBBEceb2", + "poolType": "MetaStable", + "symbol": "BPT-WSTETH-WETH", + "tokens": [ + { + "address": "0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb", + "weight": "null", + "symbol": "wstETH" + }, + { + "address": "0x4200000000000000000000000000000000000006", + "weight": "null", + "symbol": "WETH" + } + ] + }, + "tokenLogoURIs": { + "0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0x1f32b1c2345538c0c6f582fcb022739c4a194ebb.png", + "0x4200000000000000000000000000000000000006": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/optimism/assets/0x4200000000000000000000000000000000000006/logo.png" + } + }, { "address": "0x21b2Ef3DC22B7bd4634205081c667e39742075E2", "network": 100, @@ -5151,35 +5279,6 @@ "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/arbitrum/assets/0x82aF49447D8a07e3bd95BD0d56f35241523fBab1/logo.png" } }, - { - "address": "0x077794c30AFECcdF5ad2Abc0588E8CEE7197b71a", - "network": 42161, - "isKilled": true, - "relativeWeightCap": "0.02", - "addedTimestamp": 1662504532, - "pool": { - "id": "0xe1b40094f1446722c424c598ac412d590e0b3ffb000200000000000000000076", - "address": "0xE1B40094F1446722c424C598aC412D590e0b3ffb", - "poolType": "Weighted", - "symbol": "20WETH-80CRE8R", - "tokens": [ - { - "address": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", - "weight": "0.2", - "symbol": "WETH" - }, - { - "address": "0xb96B904ba83DdEeCE47CAADa8B40EE6936D92091", - "weight": "0.8", - "symbol": "CRE8R" - } - ] - }, - "tokenLogoURIs": { - "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/arbitrum/assets/0x82aF49447D8a07e3bd95BD0d56f35241523fBab1/logo.png", - "0xb96B904ba83DdEeCE47CAADa8B40EE6936D92091": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0xb96b904ba83ddeece47caada8b40ee6936d92091.png" - } - }, { "address": "0x0EDF6cDd81BC3471C053341B7D8Dfd1Cb367AD93", "network": 42161, @@ -5855,7 +5954,7 @@ ] }, "tokenLogoURIs": { - "0x6CDA1D3D092811b2d48F7476adb59A6239CA9b95": "", + "0x6CDA1D3D092811b2d48F7476adb59A6239CA9b95": "https://raw.githubusercontent.com/balancer/tokenlists/main/src/assets/images/tokens/0x6cda1d3d092811b2d48f7476adb59a6239ca9b95.png", "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/arbitrum/assets/0x82aF49447D8a07e3bd95BD0d56f35241523fBab1/logo.png" } } diff --git a/src/lib/config/optimism/pools.ts b/src/lib/config/optimism/pools.ts index c287a475f5..5cf4b2d7d3 100644 --- a/src/lib/config/optimism/pools.ts +++ b/src/lib/config/optimism/pools.ts @@ -36,6 +36,11 @@ const pools: Pools = { '0xc77e5645dbe48d54afc06655e39d3fe17eb76c1c00020000000000000000005c', '0x785f08fb77ec934c01736e30546f87b4daccbe50000200000000000000000041', '0x05e7732bf9ae5592e6aa05afe8cd80f7ab0a7bea00020000000000000000005a', + '0x39965c9dab5448482cf7e002f583c812ceb53046000100000000000000000003', + '0x1d95129c18a8c91c464111fdf7d0eb241b37a9850002000000000000000000c1', + '0xd20f6f1d8a675cdca155cb07b5dc9042c467153f0002000000000000000000bc', + '0x098f32d98d0d64dba199fc1923d3bf4192e787190001000000000000000000d2', + '0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb200020000000000000000008b', ], AllowList: [], }, diff --git a/src/pages/index.vue b/src/pages/index.vue index 6c2fc45659..e2fc31c01c 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -9,7 +9,6 @@ import usePoolFilters from '@/composables/pools/usePoolFilters'; import useBreakpoints from '@/composables/useBreakpoints'; import useNetwork from '@/composables/useNetwork'; import usePools from '@/composables/pools/usePools'; -import ZkevmPromo from '@/components/contextual/pages/pools/ZkevmPromo.vue'; // COMPOSABLES const router = useRouter(); @@ -45,7 +44,6 @@ function onColumnSort(columnId: string) {

-
diff --git a/src/providers/tokens.provider.ts b/src/providers/tokens.provider.ts index 43c394f86a..29991616b9 100644 --- a/src/providers/tokens.provider.ts +++ b/src/providers/tokens.provider.ts @@ -10,8 +10,6 @@ import { toRef, toRefs, } from 'vue'; -import { captureException } from '@sentry/browser'; - import useAllowancesQuery from '@/composables/queries/useAllowancesQuery'; import useBalancesQuery from '@/composables/queries/useBalancesQuery'; import useTokenPricesQuery, { @@ -398,10 +396,6 @@ export const tokensProvider = ( try { const price = selectByAddressFast(prices.value, getAddress(address)); if (!price) { - captureException(new Error('Could not find price for token'), { - level: 'info', - extra: { address }, - }); return 0; } return price; From 1d88db8eb695a94f1f16ca99968637c4011cadc1 Mon Sep 17 00:00:00 2001 From: Gareth Fuller Date: Thu, 6 Jul 2023 08:57:24 +0100 Subject: [PATCH 09/16] refactor: Optimise pool page queries (#3634) * refactor: Use single layout for any page with pool id in route * chore: Use pool provider for pool page * chore: Prevent refetch on focus & don't wait for token injection * refactor: Wait for vebal injection before enabling queries --- src/App.vue | 6 +-- .../pages/pool/MyPoolBalancesCard.vue | 2 +- src/components/layouts/DefaultLayout.vue | 28 +++++++++++ src/composables/queries/usePoolQuery.ts | 4 +- src/pages/_layouts/DefaultLayout.vue | 35 ++++---------- .../{JoinExitLayout.vue => PoolLayout.vue} | 16 ++++++- src/pages/pool/_id.vue | 48 ++++--------------- src/plugins/router/index.ts | 5 +- src/providers/local/pool.provider.ts | 21 ++++++++ src/providers/tokens.provider.ts | 9 +++- 10 files changed, 96 insertions(+), 78 deletions(-) create mode 100644 src/components/layouts/DefaultLayout.vue rename src/pages/_layouts/{JoinExitLayout.vue => PoolLayout.vue} (66%) 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/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/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/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/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 @@ + + 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..e8f699554b --- /dev/null +++ b/src/components/contextual/pages/vebal/cross-chain-boost/StakingCardSyncAlert.vue @@ -0,0 +1,129 @@ + + + \ 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/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/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/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/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/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/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/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/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/vebal.vue b/src/pages/vebal.vue index 849a143103..0832389896 100644 --- a/src/pages/vebal.vue +++ b/src/pages/vebal.vue @@ -5,18 +5,30 @@ import Hero from '@/components/contextual/pages/vebal/Hero.vue'; import LMVoting from '@/components/contextual/pages/vebal/LMVoting/LMVoting.vue'; import MyVeBAL from '@/components/contextual/pages/vebal/MyVeBAL/MyVeBAL.vue'; +import CrossChainBoostCards from '@/components/contextual/pages/vebal/cross-chain-boost/CrossChainBoostCards.vue'; import { isVeBalSupported } from '@/composables/useVeBAL'; + +import { provideUserStaking } from '@/providers/local/user-staking.provider'; +import { providerUserPools } from '@/providers/local/user-pools.provider'; +import { providePoolStaking } from '@/providers/local/pool-staking.provider'; + +const userStaking = provideUserStaking(); +providerUserPools(userStaking); +providePoolStaking();