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

feat(trading): gas fee estimation for withdraw transaction #5668

Merged
merged 7 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions libs/assets/src/lib/assets-data-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AssetsDocument, type AssetsQuery } from './__generated__/Assets';
import { AssetStatus } from '@vegaprotocol/types';
import { type Asset } from './asset-data-provider';
import { DENY_LIST } from './constants';
import { type AssetFieldsFragment } from './__generated__/Asset';

export interface BuiltinAssetSource {
__typename: 'BuiltinAsset';
Expand Down Expand Up @@ -89,3 +90,24 @@ export const useEnabledAssets = () => {
variables: undefined,
});
};

/** Wrapped ETH symbol */
const WETH = 'WETH';
type WETHDetails = Pick<AssetFieldsFragment, 'symbol' | 'decimals' | 'quantum'>;
/**
* Tries to find WETH asset configuration on Vega in order to provide its
* details, otherwise it returns hardcoded values.
*/
export const useWETH = (): WETHDetails => {
const { data } = useAssetsDataProvider();
if (data) {
const weth = data.find((a) => a.symbol.toUpperCase() === WETH);
if (weth) return weth;
}

return {
symbol: WETH,
decimals: 18,
quantum: '500000000000000', // 1 WETH ~= 2000 qUSD
};
};
8 changes: 7 additions & 1 deletion libs/i18n/src/locales/en/withdraws.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,11 @@
"Withdrawals of {{threshold}} {{symbol}} or more will be delayed for {{delay}}.": "Withdrawals of {{threshold}} {{symbol}} or more will be delayed for {{delay}}.",
"Withdrawals ready": "Withdrawals ready",
"You have no assets to withdraw": "You have no assets to withdraw",
"Your funds have been unlocked for withdrawal - <0>View in block explorer<0>": "Your funds have been unlocked for withdrawal - <0>View in block explorer<0>"
"Your funds have been unlocked for withdrawal - <0>View in block explorer<0>": "Your funds have been unlocked for withdrawal - <0>View in block explorer<0>",
"Gas fee": "Gas fee",
"Estimated gas fee for the withdrawal transaction (refreshes each 15 seconds)": "Estimated gas fee for the withdrawal transaction (refreshes each 15 seconds)",
"It seems that the current gas prices are exceeding the amount you're trying to withdraw": "It seems that the current gas prices are exceeding the amount you're trying to withdraw",
"The current gas price range": "The current gas price range",
"min": "min",
"max": "max"
}
42 changes: 42 additions & 0 deletions libs/utils/src/lib/format/ether.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import BigNumber from 'bignumber.js';
import { EtherUnit, formatEther, unitiseEther } from './ether';

describe('unitiseEther', () => {
it.each([
[1, '1', EtherUnit.wei],
[999, '999', EtherUnit.wei],
[1000, '1', EtherUnit.kwei],
[9999, '9.999', EtherUnit.kwei],
[10000, '10', EtherUnit.kwei],
[999999, '999.999', EtherUnit.kwei],
[1000000, '1', EtherUnit.mwei],
[999999999, '999.999999', EtherUnit.mwei],
[1000000000, '1', EtherUnit.gwei],
['999999999999999999', '999999999.999999999', EtherUnit.gwei], // max gwei
[1e18, '1', EtherUnit.ether], // 1 ETH
[1234e18, '1234', EtherUnit.ether], // 1234 ETH
])('unitises %s to [%s, %s]', (value, expectedOutput, expectedUnit) => {
const [output, unit] = unitiseEther(value);
expect(output.toFixed()).toEqual(expectedOutput);
expect(unit).toEqual(expectedUnit);
});

it('unitises to requested unit', () => {
const [output, unit] = unitiseEther(1, EtherUnit.kwei);
expect(output).toEqual(BigNumber(0.001));
expect(unit).toEqual(EtherUnit.kwei);
});
});

describe('formatEther', () => {
it.each([
[1, EtherUnit.wei, '1 wei'],
[12, EtherUnit.kwei, '12 kwei'],
[123, EtherUnit.gwei, '123 gwei'],
[3, EtherUnit.ether, '3 ETH'],
[234.67776331, EtherUnit.gwei, '235 gwei'],
[12.12, EtherUnit.gwei, '12 gwei'],
])('formats [%s, %s] to "%s"', (value, unit, expectedOutput) => {
expect(formatEther([BigNumber(value), unit])).toEqual(expectedOutput);
});
});
84 changes: 84 additions & 0 deletions libs/utils/src/lib/format/ether.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { formatNumber, toBigNum } from './number';
import type BigNumber from 'bignumber.js';

export enum EtherUnit {
/** 1 wei = 10^-18 ETH */
wei = '0',
/** 1 kwei = 1000 wei */
kwei = '3',
/** 1 mwei = 1000 kwei */
mwei = '6',
/** 1 gwei = 1000 kwei */
gwei = '9',

// other denominations:
// microether = '12', // aka szabo, µETH
// milliether = '15', // aka finney, mETH

/** 1 ETH = 1B gwei = 10^18 wei */
ether = '18',
}

export const etherUnitMapping: Record<EtherUnit, string> = {
[EtherUnit.wei]: 'wei',
[EtherUnit.kwei]: 'kwei',
[EtherUnit.mwei]: 'mwei',
[EtherUnit.gwei]: 'gwei',
// [EtherUnit.microether]: 'µETH', // szabo
// [EtherUnit.milliether]: 'mETH', // finney
[EtherUnit.ether]: 'ETH',
};

type InputValue = string | number | BigNumber;
type UnitisedTuple = [value: BigNumber, unit: EtherUnit];

/**
* Converts given raw value to the unitised tuple of amount and unit
*/
export const unitiseEther = (
input: InputValue,
forceUnit?: EtherUnit
): UnitisedTuple => {
const units = Object.values(EtherUnit).reverse();

let value = toBigNum(input, Number(forceUnit || EtherUnit.ether));
let unit = forceUnit || EtherUnit.ether;

if (!forceUnit) {
for (const u of units) {
const v = toBigNum(input, Number(u));
value = v;
unit = u;
if (v.isGreaterThanOrEqualTo(1)) break;
}
}

return [value, unit];
};

/**
* `formatNumber` wrapper for unitised ether values (attaches unit name)
*/
export const formatEther = (
input: UnitisedTuple,
decimals = 0,
noUnit = false
) => {
const [value, unit] = input;
const num = formatNumber(value, decimals);
const unitName = noUnit ? '' : etherUnitMapping[unit];

return `${num} ${unitName}`.trim();
};

/**
* Utility function that formats given raw amount as ETH.
* Example:
* Given value of `1` this will return `0.000000000000000001 ETH`
*/
export const asETH = (input: InputValue, noUnit = false) =>
formatEther(
unitiseEther(input, EtherUnit.ether),
Number(EtherUnit.ether),
noUnit
);
1 change: 1 addition & 0 deletions libs/utils/src/lib/format/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './range';
export * from './size';
export * from './strings';
export * from './trigger';
export * from './ether';
20 changes: 20 additions & 0 deletions libs/utils/src/lib/format/number.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
toDecimal,
toNumberParts,
formatNumberRounded,
toQUSD,
} from './number';

describe('number utils', () => {
Expand Down Expand Up @@ -282,3 +283,22 @@ describe('formatNumberRounded', () => {
);
});
});

describe('toQUSD', () => {
it.each([
[0, 0, 0],
[1, 1, 1],
[1, 10, 0.1],
[1, 100, 0.01],
// real life examples
[1000000, 1000000, 1], // USDC -> 1 USDC ~= 1 qUSD
[500000, 1000000, 0.5], // USDC => 0.6 USDC ~= 0.5 qUSD
[1e18, 1e18, 1], // VEGA -> 1 VEGA ~= 1 qUSD
[123.45e18, 1e18, 123.45], // VEGA -> 1 VEGA ~= 1 qUSD
[1e18, 5e14, 2000], // WETH -> 1 WETH ~= 2000 qUSD
[1e9, 5e14, 0.000002], // gwei -> 1 gwei ~= 0.000002 qUSD
[50000e9, 5e14, 0.1], // gwei -> 50000 gwei ~= 0.1 qUSD
])('converts (%d, %d) to %d qUSD', (amount, quantum, expected) => {
expect(toQUSD(amount, quantum).toNumber()).toEqual(expected);
});
});
23 changes: 22 additions & 1 deletion libs/utils/src/lib/format/number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function toDecimal(numberOfDecimals: number) {
}

export function toBigNum(
rawValue: string | number,
rawValue: string | number | BigNumber,
decimals: number
): BigNumber {
const divides = new BigNumber(10).exponentiatedBy(decimals);
Expand Down Expand Up @@ -233,3 +233,24 @@ export const formatNumberRounded = (

return value;
};

/**
* Converts given amount in one asset (determined by raw amount
* and quantum values) to qUSD.
* @param amount The raw amount
* @param quantum The quantum value of the asset.
*/
export const toQUSD = (
amount: string | number | BigNumber,
quantum: string | number
) => {
const value = new BigNumber(amount);
let q = new BigNumber(quantum);

if (q.isNaN() || q.isLessThanOrEqualTo(0)) {
q = new BigNumber(1);
}

const qUSD = value.dividedBy(q);
return qUSD;
};
1 change: 1 addition & 0 deletions libs/web3/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export * from './lib/use-ethereum-transaction';
export * from './lib/use-ethereum-withdraw-approval-toasts';
export * from './lib/use-ethereum-withdraw-approvals-manager';
export * from './lib/use-ethereum-withdraw-approvals-store';
export * from './lib/use-gas-price';
export * from './lib/use-get-withdraw-delay';
export * from './lib/use-get-withdraw-threshold';
export * from './lib/use-token-contract';
Expand Down
111 changes: 111 additions & 0 deletions libs/web3/src/lib/use-gas-price.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { useEffect, useState } from 'react';
import { useWeb3React } from '@web3-react/core';
import { useEthereumConfig } from './use-ethereum-config';
import BigNumber from 'bignumber.js';

const DEFAULT_INTERVAL = 15000; // 15 seconds

/**
* These are the hex values of the collateral bridge contract methods.
*
* Collateral bridge address: 0x23872549cE10B40e31D6577e0A920088B0E0666a
* Etherscan: https://etherscan.io/address/0x23872549cE10B40e31D6577e0A920088B0E0666a#writeContract
*/
export enum ContractMethod {
asiaznik marked this conversation as resolved.
Show resolved Hide resolved
DEPOSIT_ASSET = '0xf7683932',
EXEMPT_DEPOSITOR = '0xb76fbb75',
GLOBAL_RESUME = '0xd72ed529',
GLOBAL_STOP = '0x9dfd3c88',
LIST_ASSET = '0x0ff3562c',
REMOVE_ASSET = '0xc76de358',
REVOKE_EXEMPT_DEPOSITOR = '0x6a1c6fa4',
SET_ASSET_LIMITS = '0x41fb776d',
SET_WITHDRAW_DELAY = '0x5a246728',
WITHDRAW_ASSET = '0x3ad90635',
}

export type GasData = {
/** The base (minimum) price of 1 unit of gas */
basePrice: BigNumber;
/** The maximum price of 1 unit of gas */
maxPrice: BigNumber;
/** The amount of gas (units) needed to process a transaction */
gas: BigNumber;
};

type Provider = NonNullable<ReturnType<typeof useWeb3React>['provider']>;

const retrieveGasData = async (
provider: Provider,
account: string,
contractAddress: string,
contractMethod: ContractMethod
) => {
try {
const data = await provider.getFeeData();
const estGasAmount = await provider.estimateGas({
to: account,
from: contractAddress,
data: contractMethod,
});

if (data.lastBaseFeePerGas && data.maxFeePerGas) {
return {
// converts also form ethers BigNumber to "normal" BigNumber
basePrice: BigNumber(data.lastBaseFeePerGas.toString()),
maxPrice: BigNumber(data.maxFeePerGas.toString()),
gas: BigNumber(estGasAmount.toString()),
};
}
} catch (err) {
// NOOP - could not get the estimated gas or the fee data from
// the network. This could happen if there's an issue with transaction
// request parameters (e.g. to/from mismatch)
}

return undefined;
};

/**
* Gets the "current" gas price from the ethereum network.
*/
export const useGasPrice = (
method: ContractMethod,
interval = DEFAULT_INTERVAL
): GasData | undefined => {
const [gas, setGas] = useState<GasData | undefined>(undefined);
const { provider, account } = useWeb3React();
const { config } = useEthereumConfig();

useEffect(() => {
if (!provider || !config || !account) return;

const retrieve = async () => {
retrieveGasData(
provider,
account,
config.collateral_bridge_contract.address,
method
).then((gasData) => {
if (gasData) {
setGas(gasData);
}
});
};
retrieve();

// Retrieves another estimation and prices in [interval] ms.
let i: ReturnType<typeof setInterval>;
if (interval > 0) {
i = setInterval(() => {
retrieve();
}, interval);
}

return () => {
if (i) clearInterval(i);
};
}, [account, config, interval, method, provider]);

return gas;
};
4 changes: 4 additions & 0 deletions libs/withdraws/src/lib/withdraw-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { useForm, Controller, useWatch } from 'react-hook-form';
import { WithdrawLimits } from './withdraw-limits';
import {
ETHEREUM_EAGER_CONNECT,
type GasData,
useWeb3ConnectStore,
useWeb3Disconnect,
} from '@vegaprotocol/web3';
Expand Down Expand Up @@ -56,6 +57,7 @@ export interface WithdrawFormProps {
delay: number | undefined;
onSelectAsset: (assetId: string) => void;
submitWithdraw: (withdrawal: WithdrawalArgs) => void;
gasPrice?: GasData;
}

const WithdrawDelayNotification = ({
Expand Down Expand Up @@ -117,6 +119,7 @@ export const WithdrawForm = ({
delay,
onSelectAsset,
submitWithdraw,
gasPrice,
}: WithdrawFormProps) => {
const t = useT();
const ethereumAddress = useEthereumAddress();
Expand Down Expand Up @@ -247,6 +250,7 @@ export const WithdrawForm = ({
delay={delay}
balance={balance}
asset={selectedAsset}
gas={gasPrice}
/>
</div>
)}
Expand Down
Loading
Loading