Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

266-feature-update-gas-estimate-in-storage-check #267

Merged
merged 11 commits into from
Dec 16, 2024
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@
"postcss-loader": "^5.3.0",
"postcss-nested": "^5.0.6",
"prettier": "^3.3.3",
"react-devtools": "^4.x.x",
"react-devtools": "^6.0.1",
"react-i18next": "^15.1.1",
"react-iconfont-cli": "^2.0.2",
"shelljs": "^0.8.5",
Expand Down
2,866 changes: 1,643 additions & 1,223 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

36 changes: 17 additions & 19 deletions src/background/controller/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import { getAuth } from 'firebase/auth';
import web3, { TransactionError } from 'web3';

import eventBus from '@/eventBus';
import { type FeatureFlags } from '@/shared/types/feature-types';
import { isValidEthereumAddress, withPrefix } from '@/shared/utils/address';
import { getHashAlgo, getSignAlgo } from '@/shared/utils/algo';
// eslint-disable-next-line import/order,no-restricted-imports
// eslint-disable-next-line no-restricted-imports
import { findAddressWithNetwork } from '@/ui/utils/modules/findAddressWithPK';
import {
keyringService,
Expand Down Expand Up @@ -54,7 +55,7 @@ import DisplayKeyring from '../service/keyring/display';
import type { NFTData, NFTModel, StorageInfo, WalletResponse } from '../service/networkModel';
import type { ConnectedSite } from '../service/permission';
import type { Account } from '../service/preference';
import { StorageEvaluator } from '../service/storage-evaluator';
import { type EvaluateStorageResult, StorageEvaluator } from '../service/storage-evaluator';
import type { UserInfoStore } from '../service/user';
import defaultConfig from '../utils/defaultConfig.json';
import { getStoragedAccount } from '../utils/getStoragedAccount';
Expand Down Expand Up @@ -3875,6 +3876,10 @@ export class WalletController extends BaseController {
}
};

getFeatureFlags = async (): Promise<FeatureFlags> => {
return openapiService.getFeatureFlags();
};

allowLilicoPay = async (): Promise<boolean> => {
const isFreeGasFeeKillSwitch = await storage.get('freeGas');
const isFreeGasFeeEnabled = await storage.get('lilicoPayer');
Expand Down Expand Up @@ -4072,24 +4077,17 @@ export class WalletController extends BaseController {
transferAmount?: number; // amount in coins
coin?: string; // coin name
movingBetweenEVMAndFlow?: boolean; // are we moving between EVM and Flow?
} = {}): Promise<{
isStorageSufficient: boolean;
isStorageSufficientAfterAction: boolean;
storageInfo: StorageInfo;
}> => {
} = {}): Promise<EvaluateStorageResult> => {
const address = await this.getCurrentAddress();
const { isStorageSufficient, isStorageSufficientAfterAction, storageInfo } =
await this.storageEvaluator.evaluateStorage(
address!,
transferAmount,
coin,
movingBetweenEVMAndFlow
);
return {
isStorageSufficient,
isStorageSufficientAfterAction,
storageInfo,
};
const isFreeGasFeeEnabled = await this.allowLilicoPay();
const result = await this.storageEvaluator.evaluateStorage(
address!,
transferAmount,
coin,
movingBetweenEVMAndFlow,
isFreeGasFeeEnabled
);
return result;
};

// Tracking stuff
Expand Down
14 changes: 13 additions & 1 deletion src/background/service/conditions-evaluator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { NewsConditionType } from '@/shared/types/news-types';

import { userWalletService } from '../service';
import { StorageEvaluator } from '../service/storage-evaluator';

import type { NewsConditionType } from './networkModel';
import openapi from './openapi';

const CURRENT_VERSION = chrome.runtime.getManifest().version;
Expand All @@ -25,6 +26,11 @@ class ConditionsEvaluator {
if (!currentAddress) return false;
return this.evaluateStorageCondition(currentAddress);
}
case 'insufficientBalance': {
const currentAddress = userWalletService.getCurrentAddress();
if (!currentAddress) return false;
return this.evaluateBalanceCondition(currentAddress);
}

case 'unknown':
default:
Expand Down Expand Up @@ -66,6 +72,12 @@ class ConditionsEvaluator {
const { isStorageSufficient } = await storageEvaluator.evaluateStorage(address);
return !isStorageSufficient;
}

async evaluateBalanceCondition(address: string): Promise<boolean> {
const storageEvaluator = new StorageEvaluator();
const { isBalanceSufficient } = await storageEvaluator.evaluateStorage(address);
return !isBalanceSufficient;
}
}

export default new ConditionsEvaluator();
40 changes: 7 additions & 33 deletions src/background/service/networkModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,36 +357,10 @@ export interface DeviceInfo {
device_info: DeviceInfoRequest;
}

/*
* News items
*/
export type NewsType = 'message' | 'image';

export type NewsPriority = 'urgent' | 'high' | 'medium' | 'low';

export type NewsDisplayType =
| 'once' // show once
| 'click' // close it when user click on it
| 'expiry'; // it will display until it expired

export type NewsConditionType =
| 'unknown'
| 'canUpgrade'
| 'isIOS'
| 'isAndroid'
| 'isWeb'
| 'insufficientStorage';

export interface NewsItem {
id: string;
priority: NewsPriority;
type: NewsType;
title: string;
body?: string;
icon?: string;
image?: string;
url?: string;
expiryTime: Date;
displayType: NewsDisplayType;
conditions?: NewsConditionType[];
}
export {
type NewsItem,
type NewsPriority,
type NewsType,
type NewsDisplayType,
type NewsConditionType,
} from '@/shared/types/news-types';
26 changes: 17 additions & 9 deletions src/background/service/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { TokenInfo } from 'flow-native-token-registry';
import log from 'loglevel';

import { storage } from '@/background/webapi';
import { type FeatureFlagKey, type FeatureFlags } from '@/shared/types/feature-types';
import { isValidFlowAddress, isValidEthereumAddress } from '@/shared/utils/address';
import { getStringFromHashAlgo, getStringFromSignAlgo } from '@/shared/utils/algo';
import { getPeriodFrequency } from '@/shared/utils/getPeriodFrequency';
Expand Down Expand Up @@ -1332,16 +1333,23 @@ class OpenApiService {
return tokenList.find((item) => item.id === contract_name);
};

getFeatureFlags = async (): Promise<FeatureFlags> => {
try {
const config = await remoteFetch.remoteConfig();
return config.features;
} catch (err) {
console.error(err);
}
// By default, all feature flags are disabled
return {};
};
getFeatureFlag = async (featureFlag: FeatureFlagKey): Promise<boolean> => {
const flags = await this.getFeatureFlags();
return !!flags[featureFlag];
};

getSwapInfo = async (): Promise<boolean> => {
remoteFetch
.remoteConfig()
.then((res) => {
return res.features.swap;
})
.catch((err) => {
console.log('getNFTCollectionInfo -->', err);
});
return false;
return (await this.getFeatureFlags()).swap;
};

// @ts-ignore
Expand Down
61 changes: 40 additions & 21 deletions src/background/service/storage-evaluator.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,72 @@
import * as fcl from '@onflow/fcl';

import { openapiService } from '../service';

import type { StorageInfo } from './networkModel';
interface EvaluateStorageResult {

export type EvaluateStorageResult = {
isStorageSufficient: boolean;
isBalanceSufficient: boolean;
isStorageSufficientAfterAction: boolean;
storageInfo: StorageInfo;
}
};

export class StorageEvaluator {
private static MINIMUM_FLOW_BALANCE = 0.001;
private static MINIMUM_STORAGE_BUFFER = 10000; // minimum required storage buffer (10,000 bytes)
private static FIXED_MOVE_FEE = 0.001;
private static AVERAGE_TX_FEE = 0.001;
private static AVERAGE_TX_FEE = 0.0005;
private static BYTES_PER_FLOW = 100 * 1024 * 1024; // 100 MB

async evaluateStorage(
address: string,
sendAmount?: number,
coin?: string,
movingBetweenEVMAndFlow?: boolean
movingBetweenEVMAndFlow?: boolean,
freeGas?: boolean
): Promise<EvaluateStorageResult> {
// Get storage info from openapi service
const storageInfo = await openapiService.getStorageInfo(address);

const remainingStorage = storageInfo.capacity - storageInfo.used;
const isStorageSufficient = remainingStorage >= StorageEvaluator.MINIMUM_STORAGE_BUFFER;

let noStorageAfterAction = false;
// Calculate the flow balance that is used by the storage calculation
// I don't "love" this approach as it involves a division, but it
// avoids having to figure out which flow balance is used by the storage calculation
const flowBalanceAffectingStorage = storageInfo.capacity / StorageEvaluator.BYTES_PER_FLOW;

// Check if the flow balance is sufficient
const isBalanceSufficient =
flowBalanceAffectingStorage >= StorageEvaluator.MINIMUM_FLOW_BALANCE;

let isStorageSufficientAfterAction = true;

// Check feature flag
const FEATURE_FLAG_TX_WARNING_PREDICTION =
await openapiService.getFeatureFlag('tx_warning_prediction');

if (isStorageSufficient) {
// Check if there is enough storage after the action
if (sendAmount !== undefined) {
// This is the amount of flow that will be used by the transaction
const flowUsed =
(coin === 'flow' ? sendAmount : 0) +
(movingBetweenEVMAndFlow ? StorageEvaluator.FIXED_MOVE_FEE : 0) +
StorageEvaluator.AVERAGE_TX_FEE;
if (FEATURE_FLAG_TX_WARNING_PREDICTION) {
// The feature is enabled, so we need to check if there is enough storage after the action
if (isStorageSufficient) {
// Check if there is enough storage after the action
if (sendAmount !== undefined) {
// This is the amount of flow that will be used by the transaction
const flowUsed =
(coin === 'flow' ? sendAmount : 0) +
(movingBetweenEVMAndFlow ? StorageEvaluator.FIXED_MOVE_FEE : 0) +
(freeGas ? 0 : StorageEvaluator.AVERAGE_TX_FEE);

const storageAffected = flowUsed * StorageEvaluator.BYTES_PER_FLOW;
const remainingStorageAfterAction = storageInfo.available - storageAffected;
const storageAffected = flowUsed * StorageEvaluator.BYTES_PER_FLOW;
const remainingStorageAfterAction = storageInfo.available - storageAffected;

noStorageAfterAction =
remainingStorageAfterAction < StorageEvaluator.MINIMUM_STORAGE_BUFFER;
isStorageSufficientAfterAction =
remainingStorageAfterAction >= StorageEvaluator.MINIMUM_STORAGE_BUFFER;
}
}
}

return {
isStorageSufficient,
isStorageSufficientAfterAction: !noStorageAfterAction,
isBalanceSufficient,
isStorageSufficientAfterAction,
storageInfo,
};
}
Expand Down
7 changes: 4 additions & 3 deletions src/background/service/userWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as fcl from '@onflow/fcl';
import { getApp } from 'firebase/app';
import { getAuth, signInAnonymously } from 'firebase/auth';

import { type ActiveChildType } from '@/shared/types/wallet-types';
import { withPrefix } from '@/shared/utils/address';
import { getHashAlgo, getSignAlgo } from '@/shared/utils/algo';
// eslint-disable-next-line no-restricted-imports
Expand Down Expand Up @@ -30,7 +31,7 @@ interface UserWalletStore {
childAccount: ChildAccount;
network: string;
monitor: string;
activeChild: any;
activeChild: ActiveChildType;
evmEnabled: boolean;
}

Expand Down Expand Up @@ -126,11 +127,11 @@ class UserWallet {
this.store.childAccount = wallet;
};

setActiveWallet = (key: any) => {
setActiveWallet = (key: ActiveChildType) => {
this.store.activeChild = key;
};

getActiveWallet = () => {
getActiveWallet = (): ActiveChildType => {
return this.store.activeChild;
};

Expand Down
8 changes: 8 additions & 0 deletions src/shared/types/feature-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Example of features we use, but could be any string key as we add more
export type FeatureFlagKey = 'free_gas' | 'swap' | 'tx_warning_prediction' | string;

// Feature flags
export type FeatureFlags = {
// Other feature flags
[key: FeatureFlagKey]: boolean;
};
34 changes: 34 additions & 0 deletions src/shared/types/news-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* News items
*/
export type NewsType = 'message' | 'image';

export type NewsPriority = 'urgent' | 'high' | 'medium' | 'low';

export type NewsDisplayType =
| 'once' // show once
| 'click' // close it when user click on it
| 'expiry'; // it will display until it expired

export type NewsConditionType =
| 'unknown'
| 'canUpgrade'
| 'isIOS'
| 'isAndroid'
| 'isWeb'
| 'insufficientStorage'
| 'insufficientBalance';

export interface NewsItem {
id: string;
priority: NewsPriority;
type: NewsType;
title: string;
body?: string;
icon?: string;
image?: string;
url?: string;
expiryTime: Date;
displayType: NewsDisplayType;
conditions?: NewsConditionType[];
}
5 changes: 5 additions & 0 deletions src/shared/types/wallet-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Matches exactly 16 hex characters, with optional 0x prefix
export type FlowAddress = `0x${string & { length: 16 }}` | `${string & { length: 16 }}`;

// ActiveChildType is the type of the active child in the wallet. It can be 'evm', a FlowAddress, or null.
export type ActiveChildType = 'evm' | FlowAddress | null;
3 changes: 1 addition & 2 deletions src/ui/FRWComponent/WarningStorageLowSnackbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export const WarningStorageLowSnackbar = ({
return null;
}
return (
// <WarningSnackbar open={true} onClose={() => {}} alertIcon={warningIcon} message={message} />
<></>
<WarningSnackbar open={true} onClose={() => {}} alertIcon={warningIcon} message={message} />
);
};
Loading
Loading