From 5e5239d8504e5e74245c9dda3d6f5a3c1af91310 Mon Sep 17 00:00:00 2001 From: John Feras Date: Thu, 25 Apr 2024 15:51:00 -0400 Subject: [PATCH] Detect Umbra relayer/API version change and force reload (#653) * Detect Umbra relayer/API version change and force reload * Change reload alert to a more user-oriented message * Removed superfluous typedef on apiVersionfromSettings * Updated localhost comment for clarity --- .../src/components/AccountReceiveTable.vue | 2 +- frontend/src/components/models.ts | 11 +++++-- frontend/src/store/settings.ts | 23 ++++++++++++- frontend/src/utils/umbra-api.ts | 33 ++++++++++++++++++- 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/AccountReceiveTable.vue b/frontend/src/components/AccountReceiveTable.vue index faa5ca648..8538fa753 100644 --- a/frontend/src/components/AccountReceiveTable.vue +++ b/frontend/src/components/AccountReceiveTable.vue @@ -512,7 +512,7 @@ function useReceivedFundsTable(userAnnouncements: Ref, spend const getFeeEstimate = async (tokenAddress: string) => { if (isNativeToken(tokenAddress)) { // no fee for native token - activeFee.value = { fee: '0', token: NATIVE_TOKEN.value }; + activeFee.value = { umbraApiVersion: { major: 0, minor: 0, patch: 0 }, fee: '0', token: NATIVE_TOKEN.value }; return; } isFeeLoading.value = true; diff --git a/frontend/src/components/models.ts b/frontend/src/components/models.ts index 423716cec..fb5bd964e 100644 --- a/frontend/src/components/models.ts +++ b/frontend/src/components/models.ts @@ -170,16 +170,23 @@ export interface CnsQueryResponse { // Relayer types export type ApiError = { error: string }; +export interface UmbraApiVersion { + major: number; + minor: number; + patch: number; +} + export interface TokenInfoExtended extends TokenInfo { minSendAmount: string; } // Omit the TokenList.tokens type so we can override it with our own. export interface TokenListSuccessResponse extends Omit { + umbraApiVersion: UmbraApiVersion; nativeTokenMinSendAmount: string; tokens: TokenInfoExtended[]; } export type TokenListResponse = TokenListSuccessResponse | ApiError; -export type FeeEstimate = { fee: string; token: TokenInfo }; +export type FeeEstimate = { umbraApiVersion: UmbraApiVersion; fee: string; token: TokenInfo }; export type FeeEstimateResponse = FeeEstimate | ApiError; export type WithdrawalInputs = { stealthAddr: string; @@ -187,7 +194,7 @@ export type WithdrawalInputs = { signature: string; sponsorFee: string; }; -export type RelayResponse = { relayTransactionHash: string } | ApiError; +export type RelayResponse = { umbraApiVersion: UmbraApiVersion; relayTransactionHash: string } | ApiError; export type SendTableMetadataRow = { dateSent: string; diff --git a/frontend/src/store/settings.ts b/frontend/src/store/settings.ts index 60f73b916..308bf265d 100644 --- a/frontend/src/store/settings.ts +++ b/frontend/src/store/settings.ts @@ -2,7 +2,7 @@ import { computed, onMounted, ref } from 'vue'; import { Dark, LocalStorage } from 'quasar'; import { isHexString } from 'src/utils/ethers'; import { i18n } from '../boot/i18n'; -import { Language } from '../components/models'; +import { Language, UmbraApiVersion } from '../components/models'; // Local storage key names const settings = { @@ -11,6 +11,7 @@ const settings = { lastWallet: 'last-wallet', language: 'language', sendHistorySave: 'send-history-save', + UmbraApiVersion: 'umbra-api-version', }; // Shared state between instances @@ -119,6 +120,23 @@ export default function useSettingsStore() { scanPrivateKey.value = undefined; } + function getUmbraApiVersion(): UmbraApiVersion | null { + const storedVersion = LocalStorage.getItem(settings.UmbraApiVersion); + if (storedVersion) { + return storedVersion as UmbraApiVersion; + } else { + return null; + } + } + + function setUmbraApiVersion(version: UmbraApiVersion) { + LocalStorage.set(settings.UmbraApiVersion, version); + } + + function clearUmbraApiVersion() { + LocalStorage.remove(settings.UmbraApiVersion); + } + return { toggleDarkMode, toggleAdvancedMode, @@ -137,5 +155,8 @@ export default function useSettingsStore() { endBlock: computed(() => endBlock.value), scanPrivateKey: computed(() => scanPrivateKey.value), lastWallet: computed(() => lastWallet.value), + getUmbraApiVersion, + setUmbraApiVersion, + clearUmbraApiVersion, }; } diff --git a/frontend/src/utils/umbra-api.ts b/frontend/src/utils/umbra-api.ts index 92a92c421..a5d81a214 100644 --- a/frontend/src/utils/umbra-api.ts +++ b/frontend/src/utils/umbra-api.ts @@ -10,10 +10,16 @@ import { RelayResponse, TokenListResponse, WithdrawalInputs, + UmbraApiVersion, } from 'components/models'; import { jsonFetch } from 'src/utils/utils'; +import useSettingsStore from 'src/store/settings'; + +const { getUmbraApiVersion, setUmbraApiVersion, clearUmbraApiVersion } = useSettingsStore(); + export class UmbraApi { + // use 'http://localhost:3000' for baseUrl value for testing with a local Umbra API static baseUrl = 'https://mainnet.api.umbra.cash'; // works for all networks constructor( readonly tokens: TokenInfoExtended[], @@ -21,6 +27,25 @@ export class UmbraApi { readonly nativeTokenMinSendAmount: string | undefined ) {} + static checkUmbraApiVersion(version: UmbraApiVersion) { + const apiVersionFromSettings = getUmbraApiVersion(); + if (!apiVersionFromSettings) { + console.log(`UmbraAPI: no saved version setting, using backend value: ${version.major}.${version.minor}`); + setUmbraApiVersion(version); + } else { + console.log(`UmbraAPI: major.minor version fetched from backend: ${version.major}.${version.minor}`); + console.log( + `UmbraAPI: major.minor version fetched from setting: ${apiVersionFromSettings.major}.${apiVersionFromSettings.minor}` + ); + if (apiVersionFromSettings.major != version.major || apiVersionFromSettings.minor != version.minor) { + console.log('UmbraAPI: version mismatch, clearing settings and going to force a page reload'); + clearUmbraApiVersion(); + alert('UmbraAPI: version outdated, please reload the page'); + window.location.reload(); + } + } + } + static async create(provider: Provider | StaticJsonRpcProvider) { // Get API URL based on chain ID const chainId = (await provider.getNetwork()).chainId; @@ -37,6 +62,7 @@ export class UmbraApi { } else { tokens = data.tokens; nativeMinSend = data.nativeTokenMinSendAmount; + UmbraApi.checkUmbraApiVersion(data.umbraApiVersion); } // Return instance, using an empty array of tokens if we could not fetch them from @@ -48,6 +74,7 @@ export class UmbraApi { const response = await fetch(`${UmbraApi.baseUrl}/tokens/${tokenAddress}/estimate?chainId=${this.chainId}`); const data = (await response.json()) as FeeEstimateResponse; if ('error' in data) throw new Error(`Could not estimate fee: ${data.error}`); + UmbraApi.checkUmbraApiVersion(data.umbraApiVersion); return data; } @@ -58,12 +85,16 @@ export class UmbraApi { const response = await fetch(url, { method: 'POST', body, headers }); const data = (await response.json()) as RelayResponse; if ('error' in data) throw new Error(`Could not relay withdraw: ${data.error}`); + UmbraApi.checkUmbraApiVersion(data.umbraApiVersion); return data; } static async isGitcoinContributor(address: string) { - return (await jsonFetch(`${this.baseUrl}/addresses/${address}/is-gitcoin-contributor`)) as { + const response = (await jsonFetch(`${this.baseUrl}/addresses/${address}/is-gitcoin-contributor`)) as { + umbraApiVersion: UmbraApiVersion; isContributor: boolean; }; + UmbraApi.checkUmbraApiVersion(response.umbraApiVersion); + return response; } }