Skip to content

Commit

Permalink
EVM permit and nonce rework
Browse files Browse the repository at this point in the history
  • Loading branch information
jvonasek committed Oct 3, 2024
1 parent 1fdc604 commit bb6c4a1
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 130 deletions.
88 changes: 14 additions & 74 deletions src/api/transaction.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
import { SubmittableExtrinsic } from "@polkadot/api/promise/types"
import {
useAccount,
useEvmAccount,
useWallet,
} from "sections/web3-connect/Web3Connect.utils"
import { useAccount, useWallet } from "sections/web3-connect/Web3Connect.utils"
import { AccountId32 } from "@polkadot/types/interfaces"
import { useMutation, useQueries, useQuery } from "@tanstack/react-query"
import { QUERY_KEYS } from "utils/queryKeys"
import { Maybe, undefinedNoop } from "utils/helpers"
import { ApiPromise } from "@polkadot/api"
import { useRpcProvider } from "providers/rpcProvider"
import { BN_0, BN_1 } from "utils/constants"
import {
EthereumSigner,
PermitResult,
} from "sections/web3-connect/signer/EthereumSigner"
import { create } from "zustand"
import { EthereumSigner } from "sections/web3-connect/signer/EthereumSigner"
import BigNumber from "bignumber.js"
import { useSpotPrice } from "api/spotPrice"
import { useAccountCurrency } from "api/payments"
Expand Down Expand Up @@ -57,49 +49,28 @@ export function useMultiplePaymentInfo(
return useQueries({ queries })
}

export function useNextEvmPermitNonce() {
const { account } = useEvmAccount()
const address = account?.address ?? ""
export function useNextEvmPermitNonce(account: Maybe<AccountId32 | string>) {
const { wallet } = useWallet()
const {
permitNonce,
pendingPermit,
setPermitNonce,
incrementPermitNonce,
revertPermitNonce,
setPendingPermit,
} = useEvmPermitStore()

const isEvmSigner = wallet?.signer instanceof EthereumSigner
const currentPermitNonce = permitNonce[address]

useQuery(
QUERY_KEYS.nextEvmPermitNonce(address),
return useQuery(
QUERY_KEYS.nextEvmPermitNonce(account),
async () => {
if (!address) throw new Error("Missing address")
if (!account) throw new Error("Missing address")
if (!wallet?.signer) throw new Error("Missing wallet signer")
if (!isEvmSigner) throw new Error("Invalid signer")
return await wallet.signer.getPermitNonce()
if (!(wallet?.signer instanceof EthereumSigner))
throw new Error("Invalid signer")

return wallet.signer.getPermitNonce()
},
{
enabled: !!address && isEvmSigner && !currentPermitNonce,
cacheTime: 1000 * 60 * 60 * 24,
keepPreviousData: true,
onSuccess: (nonce) => {
if (address && nonce) {
setPermitNonce(address, nonce)
}
},
cacheTime: 0,
staleTime: 0,
initialData: BN_0,
enabled: !!account && isEvmSigner,
},
)

return {
permitNonce: currentPermitNonce ?? BN_0,
pendingPermit,
incrementPermitNonce,
revertPermitNonce,
setPendingPermit,
}
}

export function useNextNonce(account: Maybe<AccountId32 | string>) {
Expand Down Expand Up @@ -154,37 +125,6 @@ export async function getTransactionLinkFromHash(
}
}

export const useEvmPermitStore = create<{
pendingPermit: PermitResult | null
permitNonce: { [address: string]: BigNumber }
setPermitNonce: (address: string, nonce: BigNumber) => void
setPendingPermit: (permit: PermitResult | null) => void
revertPermitNonce: (address: string) => void
incrementPermitNonce: (address: string) => void
}>((set) => ({
pendingPermit: null,
permitNonce: {},
setPermitNonce: (address, nonce) =>
set(({ permitNonce }) => ({
permitNonce: { ...permitNonce, [address]: nonce },
})),
setPendingPermit: (permit) => set({ pendingPermit: permit }),
revertPermitNonce: (address) =>
set(({ permitNonce }) => ({
permitNonce: {
...permitNonce,
[address]: (permitNonce[address] ?? BN_0).minus(1),
},
})),
incrementPermitNonce: (address) =>
set(({ permitNonce }) => ({
permitNonce: {
...permitNonce,
[address]: (permitNonce[address] ?? BN_0).plus(1),
},
})),
}))

export const useEstimatedFees = (txs: SubmittableExtrinsic[]) => {
const { getAsset, native } = useAssets()
const { account } = useAccount()
Expand Down
153 changes: 101 additions & 52 deletions src/sections/transaction/ReviewTransaction.utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ import {
TransactionResponse,
} from "@ethersproject/providers"
import { chainsMap } from "@galacticcouncil/xcm-cfg"
import { Hash } from "@open-web3/orml-types/interfaces"
import { AccountId32, Hash } from "@open-web3/orml-types/interfaces"
import { ApiPromise } from "@polkadot/api"
import { SubmittableExtrinsic } from "@polkadot/api/types"
import type { AnyJson } from "@polkadot/types-codec/types"
import { ExtrinsicStatus } from "@polkadot/types/interfaces"
import { ISubmittableResult } from "@polkadot/types/types"
import { MutationObserverOptions, useMutation } from "@tanstack/react-query"
import {
MutationObserverOptions,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query"
import { useAssets } from "providers/assets"
import { useNextEvmPermitNonce } from "api/transaction"
import { useShallow } from "hooks/useShallow"
import { useRpcProvider } from "providers/rpcProvider"
import { useCallback, useRef, useState } from "react"
Expand All @@ -23,8 +27,11 @@ import { PermitResult } from "sections/web3-connect/signer/EthereumSigner"
import { useSettingsStore } from "state/store"
import { useToast } from "state/toasts"
import { H160, getEvmChainById, getEvmTxLink, isEvmAccount } from "utils/evm"
import { isAnyParachain } from "utils/helpers"
import { isAnyParachain, Maybe, sleep } from "utils/helpers"
import { createSubscanLink } from "utils/formatting"
import { QUERY_KEYS } from "utils/queryKeys"

const EVM_PERMIT_BLOCKTIME = 20_000

type TxMethod = AnyJson & {
method: string
Expand Down Expand Up @@ -222,6 +229,41 @@ export const useSendEvmTransactionMutation = (
}
}

export const usePendingDispatchPermit = (
address: Maybe<AccountId32 | string>,
) => {
const { api, isLoaded } = useRpcProvider()

return useQuery(
QUERY_KEYS.pendingEvmPermit(address),
async () => {
const raw = await api.rpc.author.pendingExtrinsics()
const pendingExtrinsics = raw.toHuman()

if (!Array.isArray(pendingExtrinsics)) return null

const pendingPermit = pendingExtrinsics.find((ext: AnyJson) => {
if (!isTxExtrinsic(ext)) return false

const evmAddress = address ? H160.fromAccount(address.toString()) : ""
const fromAddress = ext?.method?.args?.from?.toString() ?? ""

return (
ext?.method?.section === "multiTransactionPayment" &&
ext?.method?.method === "dispatchPermit" &&
fromAddress.toLowerCase() === evmAddress.toLowerCase()
)
}) as TxExtrinsic

return pendingPermit ?? null
},
{
refetchInterval: EVM_PERMIT_BLOCKTIME,
enabled: isLoaded && isEvmAccount(address?.toString()),
},
)
}

export const useSendDispatchPermit = (
options: MutationObserverOptions<
ISubmittableResult,
Expand All @@ -233,6 +275,7 @@ export const useSendDispatchPermit = (
> = {},
) => {
const { api } = useRpcProvider()
const queryClient = useQueryClient()
const [txState, setTxState] = useState<ExtrinsicStatus["type"] | null>(null)
const [txHash, setTxHash] = useState<string>("")
const [xcallMeta, setCallMeta] = useState<Record<string, string> | undefined>(
Expand All @@ -243,48 +286,63 @@ export const useSendDispatchPermit = (
const sendTx = useMutation(async ({ permit, xcallMeta }) => {
return await new Promise(async (resolve, reject) => {
try {
const unsubscribe = await api.tx.multiTransactionPayment
.dispatchPermit(
permit.message.from,
permit.message.to,
permit.message.value,
permit.message.data,
permit.message.gaslimit,
permit.message.deadline,
permit.signature.v,
permit.signature.r,
permit.signature.s,
)
.send(async (result) => {
if (!result || !result.status) return
const extrinsic = api.tx.multiTransactionPayment.dispatchPermit(
permit.message.from,
permit.message.to,
permit.message.value,
permit.message.data,
permit.message.gaslimit,
permit.message.deadline,
permit.signature.v,
permit.signature.r,
permit.signature.s,
)
const subscription = extrinsic.send(async (result) => {
if (!result || !result.status) return

const unsubscribe = await subscription

const timeout = setTimeout(() => {
clearTimeout(timeout)
reject(new UnknownTransactionState())
}, 60000)

if (isMounted()) {
setTxHash(result.txHash.toHex())
setTxState(result.status.type)
setCallMeta(xcallMeta)
} else {
clearTimeout(timeout)
}

const account = new H160(permit.message.from).toAccount()
const queryKey = QUERY_KEYS.pendingEvmPermit(account)
queryClient.setQueryData(queryKey, extrinsic.toHuman())
// stop checking for pending permits until the transaction is settled
queryClient.setQueryDefaults(queryKey, { refetchInterval: 0 })
// give some lee-way for the EVM transaction to be processed
await sleep(EVM_PERMIT_BLOCKTIME)

const timeout = setTimeout(() => {
const onComplete = createResultOnCompleteHandler(api, {
onError: async (error) => {
clearTimeout(timeout)
reject(new UnknownTransactionState())
}, 60000)

if (isMounted()) {
setTxHash(result.txHash.toHex())
setTxState(result.status.type)
setCallMeta(xcallMeta)
} else {
reject(error)
},
onSuccess: async (result) => {
clearTimeout(timeout)
}

const onComplete = createResultOnCompleteHandler(api, {
onError: (error) => {
clearTimeout(timeout)
reject(error)
},
onSuccess: (result) => {
clearTimeout(timeout)
resolve(result)
},
onSettled: unsubscribe,
})

return onComplete(result)
resolve(result)
},
onSettled: async () => {
unsubscribe()
queryClient.refetchQueries(QUERY_KEYS.nextEvmPermitNonce(account))
queryClient.setQueryDefaults(queryKey, {
refetchInterval: EVM_PERMIT_BLOCKTIME,
})
},
})

return onComplete(result)
})
} catch (err) {
reject(err?.toString() ?? "Unknown error")
}
Expand Down Expand Up @@ -527,18 +585,13 @@ const useStoreExternalAssetsOnSign = () => {
}

export const useSendTx = () => {
const { account } = useEvmAccount()
const address = account?.address ?? ""
const [txType, setTxType] = useState<"default" | "evm" | "permit" | null>(
null,
)

const boundReferralToast = useBoundReferralToast()
const storeExternalAssetsOnSign = useStoreExternalAssetsOnSign()

const { incrementPermitNonce, revertPermitNonce, setPendingPermit } =
useNextEvmPermitNonce()

const sendTx = useSendTransactionMutation({
onMutate: ({ tx }) => {
boundReferralToast.onLoading(tx)
Expand All @@ -560,13 +613,9 @@ export const useSendTx = () => {
})

const sendPermitTx = useSendDispatchPermit({
onMutate: ({ permit }) => {
onMutate: () => {
setTxType("permit")
setPendingPermit(permit)
incrementPermitNonce(address)
},
onError: () => revertPermitNonce(address),
onSettled: () => setPendingPermit(null),
})

const activeMutation =
Expand Down
9 changes: 6 additions & 3 deletions src/sections/transaction/ReviewTransactionForm.utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { useEvmPaymentFee } from "api/evm"
import { useProviderRpcUrlStore } from "api/provider"
import { useMemo } from "react"
import { useAssets } from "providers/assets"
import { usePendingDispatchPermit } from "sections/transaction/ReviewTransaction.utils"

export const useTransactionValues = ({
xcallMeta,
Expand Down Expand Up @@ -107,9 +108,10 @@ export const useTransactionValues = ({
const isSpotPriceNan = spotPrice.data?.spotPrice.isNaN()

const shouldUsePermit = isEvm && feePaymentMeta?.id !== NATIVE_EVM_ASSET_ID
const { permitNonce, pendingPermit } = useNextEvmPermitNonce()
const { data: pendingPermit } = usePendingDispatchPermit(account?.address)

const nonce = useNextNonce(account?.address)
const permitNonce = useNextEvmPermitNonce(account?.address)

const era = useEra(
boundedTx.era,
Expand All @@ -129,6 +131,7 @@ export const useTransactionValues = ({
isPaymentInfoLoading ||
spotPrice.isInitialLoading ||
nonce.isLoading ||
permitNonce.isLoading ||
acceptedFeePaymentAssets.isInitialLoading ||
referrer.isInitialLoading

Expand All @@ -152,7 +155,7 @@ export const useTransactionValues = ({
tx: boundedTx,
isNewReferralLink,
shouldUsePermit,
permitNonce,
permitNonce: permitNonce.data,
pendingPermit,
},
}
Expand Down Expand Up @@ -232,7 +235,7 @@ export const useTransactionValues = ({
tx: boundedTx,
isNewReferralLink,
shouldUsePermit,
permitNonce,
permitNonce: permitNonce.data,
pendingPermit,
},
}
Expand Down
3 changes: 3 additions & 0 deletions src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,3 +428,6 @@ export const arraysEqual = <T>(arr1: T[], arr2: T[]): boolean => {

return true
}

export const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms))
Loading

0 comments on commit bb6c4a1

Please sign in to comment.