From 16282f10f52aa1f4f0db76ba181c205fc080a8a5 Mon Sep 17 00:00:00 2001 From: 0xKheops <26880866+0xKheops@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:48:56 +0900 Subject: [PATCH] feat: fee estimate --- .../Staking/InlineStakingFeeEstimate.tsx | 30 ++++++++++ .../ui/domains/Staking/InlineStakingForm.tsx | 37 +++++++++---- .../domains/Staking/InlineStakingReview.tsx | 13 +---- .../domains/Staking/useInlineStakingWizard.ts | 39 +++++++++++++ apps/extension/src/ui/util/scaleApi.ts | 55 +++++++++++++++++-- 5 files changed, 147 insertions(+), 27 deletions(-) create mode 100644 apps/extension/src/ui/domains/Staking/InlineStakingFeeEstimate.tsx diff --git a/apps/extension/src/ui/domains/Staking/InlineStakingFeeEstimate.tsx b/apps/extension/src/ui/domains/Staking/InlineStakingFeeEstimate.tsx new file mode 100644 index 000000000..861e49dbb --- /dev/null +++ b/apps/extension/src/ui/domains/Staking/InlineStakingFeeEstimate.tsx @@ -0,0 +1,30 @@ +import { classNames } from "@talismn/util" +import { FC } from "react" + +import { TokensAndFiat } from "../Asset/TokensAndFiat" +import { useInlineStakingWizard } from "./useInlineStakingWizard" + +export const InlineStakingFeeEstimate: FC<{ noCountUp?: boolean }> = ({ noCountUp }) => { + const { feeEstimate, feeToken, isLoadingFeeEstimate, errorFeeEstimate } = useInlineStakingWizard() + + return ( + <> + {errorFeeEstimate ? ( +
Failed to estimate fee
+ ) : !!feeEstimate && !!feeToken ? ( + + ) : isLoadingFeeEstimate ? ( +
+ 0.0000 TKN ($0.00) +
+ ) : null} + + ) +} diff --git a/apps/extension/src/ui/domains/Staking/InlineStakingForm.tsx b/apps/extension/src/ui/domains/Staking/InlineStakingForm.tsx index be56f6a80..ef87d64b1 100644 --- a/apps/extension/src/ui/domains/Staking/InlineStakingForm.tsx +++ b/apps/extension/src/ui/domains/Staking/InlineStakingForm.tsx @@ -29,6 +29,7 @@ import Tokens from "../Asset/Tokens" import { TokensAndFiat } from "../Asset/TokensAndFiat" import { AccountPillButton } from "./AccountPillButton" import { InlineStakingAccountPicker } from "./InlineStakingAccountPicker" +import { InlineStakingFeeEstimate } from "./InlineStakingFeeEstimate" import { InlineStakingPoolPicker } from "./InlineStakingPoolPicker" import { useNomPoolsBondingDuration } from "./useBondingDuration" import { useInlineStakingWizard } from "./useInlineStakingWizard" @@ -419,8 +420,16 @@ const durationFromMs = (ms: number): Duration => { export const InlineStakingForm = () => { const { t } = useTranslation() - const { account, accountPicker, token, pool, poolPicker, isFormValid, setStep } = - useInlineStakingWizard() + const { + account, + accountPicker, + token, + pool, + poolPicker, + isFormValid, + + setStep, + } = useInlineStakingWizard() return (
@@ -478,14 +487,22 @@ export const InlineStakingForm = () => {
{t("Estimated Fee")}
- + + {/* {errorFeeEstimate ? ( +
Failed to estimate fee
+ ) : !!feeEstimate && !!feeToken ? ( + + ) : isLoadingFeeEstimate ? ( +
+ 0.0000 TKN ($0.00) +
+ ) : null} */}
diff --git a/apps/extension/src/ui/domains/Staking/InlineStakingReview.tsx b/apps/extension/src/ui/domains/Staking/InlineStakingReview.tsx index f326a9dc2..82ce6d73e 100644 --- a/apps/extension/src/ui/domains/Staking/InlineStakingReview.tsx +++ b/apps/extension/src/ui/domains/Staking/InlineStakingReview.tsx @@ -5,6 +5,7 @@ import { TokenLogo } from "../Asset/TokenLogo" import { TokensAndFiat } from "../Asset/TokensAndFiat" import { SapiSendButton } from "../Transactions/SapiSendButton" import { InlineStakingAccount } from "./InlineStakingAccount" +import { InlineStakingFeeEstimate } from "./InlineStakingFeeEstimate" import { useInlineStakingWizard } from "./useInlineStakingWizard" export const InlineStakingReview = () => { @@ -64,14 +65,7 @@ export const InlineStakingReview = () => {
{t("Estimated fee")}
- +
@@ -90,9 +84,6 @@ export const InlineStakingReview = () => { txMetadata={txMetadata} /> )} - {/* */} ) } diff --git a/apps/extension/src/ui/domains/Staking/useInlineStakingWizard.ts b/apps/extension/src/ui/domains/Staking/useInlineStakingWizard.ts index 730199220..63c57ffb9 100644 --- a/apps/extension/src/ui/domains/Staking/useInlineStakingWizard.ts +++ b/apps/extension/src/ui/domains/Staking/useInlineStakingWizard.ts @@ -11,6 +11,7 @@ import { useAccountByAddress } from "@ui/hooks/useAccountByAddress" import useToken from "@ui/hooks/useToken" import { useTokenRates } from "@ui/hooks/useTokenRates" +import { useFeeToken } from "../SendFunds/useFeeToken" import { useNominationPool } from "./useNominationPools" type InlineStakingWizardStep = "form" | "review" | "follow-up" @@ -72,6 +73,7 @@ export const useInlineStakingWizard = () => { const account = useAccountByAddress(state.address) const token = useToken(state.tokenId) + const feeToken = useFeeToken(token?.id) const pool = useNominationPool(token?.chain?.id, state.poolId) const tokenRates = useTokenRates(state.tokenId) const formatter = useMemo( @@ -154,6 +156,18 @@ export const useInlineStakingWizard = () => { }, }) + const { + data: feeEstimate, + isLoading: isLoadingFeeEstimate, + error: errorFeeEstimate, + } = useQuery({ + queryKey: ["feeEstimate", payloadAndMetadata?.payload], // safe stringify because contains bigint + queryFn: () => { + if (!sapi || !payloadAndMetadata?.payload) return null + return sapi.getFeeEstimate(payloadAndMetadata.payload) + }, + }) + const onSubmitted = useCallback( (hash: Hex) => { if (hash) setState((prev) => ({ ...prev, step: "follow-up", hash })) @@ -189,6 +203,24 @@ export const useInlineStakingWizard = () => { // } // }, [sapi, setState, state]) + // useEffect(() => { + // console.log("[sapi] useInlineStakingWizard", { + // payload: payloadAndMetadata?.payload, + // feeEstimate, + // isLoadingFeeEstimate, + // isLoadingPayload, + // errorPayload, + // errorFeeEstimate, + // }) + // }, [ + // errorFeeEstimate, + // errorPayload, + // feeEstimate, + // isLoadingFeeEstimate, + // isLoadingPayload, + // payloadAndMetadata?.payload, + // ]) + return { account, token, @@ -203,6 +235,8 @@ export const useInlineStakingWizard = () => { hash, isSubmitting, submitErrorMessage, + feeToken, + setAddress, setTokenId, setPoolId, @@ -214,6 +248,11 @@ export const useInlineStakingWizard = () => { ...payloadAndMetadata, isLoadingPayload, errorPayload, + + feeEstimate, + isLoadingFeeEstimate, + errorFeeEstimate, + onSubmitted, } } diff --git a/apps/extension/src/ui/util/scaleApi.ts b/apps/extension/src/ui/util/scaleApi.ts index 3e0729e51..f0948b0aa 100644 --- a/apps/extension/src/ui/util/scaleApi.ts +++ b/apps/extension/src/ui/util/scaleApi.ts @@ -7,7 +7,9 @@ import { } from "@polkadot-api/substrate-bindings" import { mergeUint8, toHex } from "@polkadot-api/utils" import { Metadata, TypeRegistry } from "@polkadot/types" -import { fromHex, getDynamicBuilder, getLookupFn, V14, V15 } from "@talismn/scale" +import { IRuntimeVersionBase } from "@polkadot/types/types" +import { getDynamicBuilder, getLookupFn, V14, V15 } from "@talismn/scale" +import { sleep } from "@talismn/util" import { ChainId, SignerPayloadJSON } from "extension-core" import { DEBUG, log } from "extension-shared" import { Binary } from "polkadot-api" @@ -15,7 +17,7 @@ import { Hex } from "viem" import { api } from "@ui/api" -import { getSignedExtensionValues } from "./signedExtensions" +import { getExtrinsicDispatchInfo } from "./getExtrinsicDispatchInfo" type ScaleMetadata = V14 | V15 type ScaleBuilder = ReturnType @@ -75,6 +77,9 @@ export const getScaleApi = ( config: PayloadSignerConfig ) => getSignerPayloadJSON(chainId, metadata, builder, pallet, method, args, config, chainInfo), + getFeeEstimate: async (payload: SignerPayloadJSON) => + getFeeEstimate(chainId, metadata, builder, payload, chainInfo), + submit: (payload: SignerPayloadJSON, signature?: Hex) => api.subSubmit(payload, signature), } } @@ -132,9 +137,9 @@ const getPayloadWithMetadataHash = ( const metadataHash = toHex(merkleizedMetadata.digest()) as Hex // TODO do this without PJS / registry - const { extra, additionalSigned } = getSignedExtensionValues(payload, metadata) - const badExtPayload = mergeUint8(fromHex(payload.method), ...extra, ...additionalSigned) - log.debug("[sapi] bad ExtPayload", { badExtPayload }) + // const { extra, additionalSigned } = getSignedExtensionValues(payload, metadata) + // const badExtPayload = mergeUint8(fromHex(payload.method), ...extra, ...additionalSigned) + // log.debug("[sapi] bad ExtPayload", { badExtPayload }) const stop2 = log.timer("get ExtrinsicPayload using PJS") const registry = new TypeRegistry() @@ -142,7 +147,7 @@ const getPayloadWithMetadataHash = ( const extPayload = registry.createType("ExtrinsicPayload", payload) const barePayload = extPayload.toU8a(true) stop2() - log.debug("[sapi] good ExtPayload", { badExtPayload }) + log.debug("[sapi] good ExtPayload", { barePayload }) const txMetadata = merkleizedMetadata.getProofForExtrinsicPayload(barePayload) @@ -219,6 +224,44 @@ const getSignerPayloadJSON = async ( return { payload, txMetadata } } +const getFeeEstimate = async ( + chainId: ChainId, + metadata: ScaleMetadata, + builder: ScaleBuilder, + payload: SignerPayloadJSON, + chainInfo: ChainInfo +) => { + const fullMetadata = { + magicNumber: 1635018093, // magic number for metadata + metadata: { tag: "v15" as const, value: metadata as V15 }, + } + const metadataBytes = metadataCodec.enc(fullMetadata) + + const stop = log.timer("[sapi] getFeeEstimate => create Extrinsic") + const registry = new TypeRegistry() + registry.setMetadata(new Metadata(registry, metadataBytes), payload.signedExtensions) + const extrinsic = registry.createType("Extrinsic", payload) + stop() + + extrinsic.signFake(payload.address, { + nonce: payload.nonce, + blockHash: payload.blockHash, + genesisHash: payload.genesisHash, + //payload, + runtimeVersion: { + specVersion: chainInfo.specVersion, + transactionVersion: chainInfo.transactionVersion, + // other fields aren't necessary for signing + } as IRuntimeVersionBase, + }) + + await sleep(2000) + + const { partialFee } = await getExtrinsicDispatchInfo(chainId, extrinsic) + + return BigInt(partialFee) +} + const getConstantValue = ( metadata: ScaleMetadata, scaleBuilder: ScaleBuilder,