Browse Source

✉️ Signer metadata (#4042)

* Worker helper function

* Integrate worker function into provider

* Initial feature implementation on tx send

* Introduce skip mechnism for signer metadata update

* Fix circular dependency

---------

Co-authored-by: attemka <attemka@gmail.com>
WRadoslaw 1 năm trước cách đây
mục cha
commit
4719659508

+ 25 - 0
packages/atlas/src/joystream-lib/lib.ts

@@ -3,7 +3,9 @@ import '@joystream/types'
 import { ApiPromise, WsProvider } from '@polkadot/api'
 import { QueryableStorageMultiArg } from '@polkadot/api-base/types/storage'
 import { Signer } from '@polkadot/api/types'
+import { getSpecTypes } from '@polkadot/types-known'
 import { Codec, SignerPayloadRawBase } from '@polkadot/types/types'
+import { base64Encode } from '@polkadot/util-crypto'
 import BN from 'bn.js'
 import { proxy } from 'comlink'
 
@@ -227,4 +229,27 @@ export class JoystreamLib {
       distributionBucketsCountPerFamily: transformedFamilies,
     }
   }
+
+  async getChainMetadata() {
+    await this.ensureApi()
+    const systemChain = await this.api.rpc.system.chain()
+
+    return {
+      icon: 'substrate',
+      chainType: 'substrate',
+      chain: systemChain.toString(),
+      metaCalls: base64Encode(this.api.runtimeMetadata.asCallsOnly.toU8a()),
+      types: getSpecTypes(
+        this.api.registry,
+        systemChain.toString(),
+        this.api.runtimeVersion.specName.toString(),
+        this.api.runtimeVersion.specVersion
+      ),
+      specVersion: this.api.runtimeVersion.specVersion.toNumber(),
+      ss58Format: this.api.registry.chainSS58 ?? 0,
+      tokenDecimals: this.api.registry.chainDecimals[0],
+      tokenSymbol: this.api.registry.chainTokens[0],
+      genesisHash: this.api.genesisHash.toHex(),
+    }
+  }
 }

+ 33 - 0
packages/atlas/src/providers/transactions/transactions.hooks.ts

@@ -13,6 +13,7 @@ import { absoluteRoutes } from '@/config/routes'
 import { ErrorCode, JoystreamLibError, JoystreamLibErrorType } from '@/joystream-lib/errors'
 import { ExtrinsicResult, ExtrinsicStatus, ExtrinsicStatusCallbackFn } from '@/joystream-lib/types'
 import { useSubscribeAccountBalance } from '@/providers/joystream/joystream.hooks'
+import { useUser } from '@/providers/user/user.hooks'
 import { useUserStore } from '@/providers/user/user.store'
 import { createId } from '@/utils/createId'
 import { ConsoleLogger, SentryLogger } from '@/utils/logs'
@@ -62,6 +63,8 @@ export const useTransaction = (): HandleTransactionFn => {
   const { displaySnackbar } = useSnackbar()
   const getMetaprotocolTxStatus = useMetaprotocolTransactionStatus()
   const { totalBalance } = useSubscribeAccountBalance()
+  const { isSignerMetadataOutdated, updateSignerMetadata, skipSignerMetadataUpdate } = useUser()
+  const { wallet } = useUserStore()
 
   return useCallback(
     async ({
@@ -84,6 +87,32 @@ export const useTransaction = (): HandleTransactionFn => {
         return false
       }
 
+      if (isSignerMetadataOutdated) {
+        await new Promise((resolve) => {
+          openOngoingTransactionModal({
+            title: 'Update Wallet Metadata',
+            type: 'informative',
+            description: `Updated metadata in ${wallet?.title} wallet will allow to view all transactions details before signing. If you choose to ignore it, you will not be prompted until next version of node update is released.`,
+            primaryButton: {
+              text: 'Update',
+              onClick: () => {
+                updateSignerMetadata().then(() => {
+                  resolve(null)
+                })
+              },
+            },
+            secondaryButton: {
+              text: 'Skip',
+              onClick: () => {
+                resolve(null)
+                skipSignerMetadataUpdate()
+              },
+            },
+          })
+        })
+        closeOngoingTransactionModal()
+      }
+
       if (fee && totalBalance?.lt(fee)) {
         displaySnackbar({
           title: 'Not enough funds',
@@ -306,13 +335,17 @@ export const useTransaction = (): HandleTransactionFn => {
       closeOngoingTransactionModal,
       displaySnackbar,
       getMetaprotocolTxStatus,
+      isSignerMetadataOutdated,
       navigate,
       nodeConnectionStatus,
       openOngoingTransactionModal,
       removeTransaction,
+      skipSignerMetadataUpdate,
       totalBalance,
+      updateSignerMetadata,
       updateTransaction,
       userWalletName,
+      wallet?.title,
     ]
   )
 }

+ 73 - 5
packages/atlas/src/providers/user/user.provider.tsx

@@ -2,6 +2,7 @@ import { FC, PropsWithChildren, createContext, useCallback, useContext, useEffec
 
 import { useMemberships } from '@/api/hooks/membership'
 import { ViewErrorFallback } from '@/components/ViewErrorFallback'
+import { JoystreamContext, JoystreamContextValue } from '@/providers/joystream/joystream.provider'
 import { isMobile } from '@/utils/browser'
 import { AssetLogger, SentryLogger } from '@/utils/logs'
 import { retryPromise } from '@/utils/misc'
@@ -16,13 +17,22 @@ UserContext.displayName = 'UserContext'
 const isMobileDevice = isMobile()
 
 export const UserProvider: FC<PropsWithChildren> = ({ children }) => {
-  const { accountId, memberId, channelId, walletAccounts, walletStatus, lastUsedWalletName } = useUserStore(
-    (state) => state
-  )
-  const { setActiveUser, setSignInModalOpen } = useUserStore((state) => state.actions)
+  const {
+    accountId,
+    memberId,
+    channelId,
+    walletAccounts,
+    walletStatus,
+    lastUsedWalletName,
+    wallet,
+    lastChainMetadataVersion,
+  } = useUserStore((state) => state)
+  const { setActiveUser, setSignInModalOpen, setLastChainMetadataVersion } = useUserStore((state) => state.actions)
   const { initSignerWallet } = useSignerWallet()
+  const joystreamCtx = useContext<JoystreamContextValue | undefined>(JoystreamContext)
 
   const [isAuthLoading, setIsAuthLoading] = useState(true)
+  const [isSignerMetadataOutdated, setIsSignerMetadataOutdated] = useState(false)
 
   const accountsIds = walletAccounts.map((a) => a.address)
 
@@ -153,6 +163,51 @@ export const UserProvider: FC<PropsWithChildren> = ({ children }) => {
   const activeChannel =
     (activeMembership && activeMembership?.channels.find((channel) => channel.id === channelId)) || null
 
+  const checkSignerStatus = useCallback(async () => {
+    const chainMetadata = await joystreamCtx?.joystream?.getChainMetadata()
+
+    if (wallet?.extension.metadata && chainMetadata) {
+      const [localGenesisHash, localSpecVersion] = lastChainMetadataVersion ?? ['', 0]
+
+      // update was skipped
+      if (localGenesisHash === chainMetadata.genesisHash && localSpecVersion === chainMetadata.specVersion) {
+        return setIsSignerMetadataOutdated(false)
+      }
+
+      const extensionMetadata = await wallet.extension.metadata.get()
+      const currentChain = extensionMetadata.find(
+        (infoEntry: { specVersion: number; genesisHash: string }) =>
+          infoEntry.genesisHash === chainMetadata?.genesisHash
+      )
+
+      // if there isn't even a metadata entry for node with specific genesis hash then update
+      if (!currentChain) {
+        return setIsSignerMetadataOutdated(true)
+      }
+
+      // if there is metadata for this node then verify specVersion
+      const isOutdated = currentChain.specVersion < chainMetadata.specVersion
+      setIsSignerMetadataOutdated(isOutdated)
+    }
+  }, [joystreamCtx?.joystream, lastChainMetadataVersion, wallet?.extension.metadata])
+
+  const updateSignerMetadata = useCallback(async () => {
+    const chainMetadata = await joystreamCtx?.joystream?.getChainMetadata()
+    return wallet?.extension.metadata.provide(chainMetadata)
+  }, [joystreamCtx?.joystream, wallet?.extension.metadata])
+
+  const skipSignerMetadataUpdate = useCallback(async () => {
+    const chainMetadata = await joystreamCtx?.joystream?.getChainMetadata()
+    if (chainMetadata) {
+      setLastChainMetadataVersion(chainMetadata.genesisHash, chainMetadata.specVersion)
+      setIsSignerMetadataOutdated(false)
+    }
+  }, [joystreamCtx?.joystream, setLastChainMetadataVersion])
+
+  useEffect(() => {
+    checkSignerStatus()
+  }, [checkSignerStatus])
+
   const isChannelBelongsToTheUserOrExists = activeMembership?.channels.length
     ? activeMembership.channels.some((channel) => channel.id === channelId)
     : true
@@ -172,8 +227,21 @@ export const UserProvider: FC<PropsWithChildren> = ({ children }) => {
       isAuthLoading,
       signIn,
       refetchUserMemberships,
+      isSignerMetadataOutdated,
+      updateSignerMetadata,
+      skipSignerMetadataUpdate,
     }),
-    [memberships, membershipsLoading, activeMembership, activeChannel, isAuthLoading, signIn, refetchUserMemberships]
+    [
+      memberships,
+      membershipsLoading,
+      activeMembership,
+      isAuthLoading,
+      signIn,
+      refetchUserMemberships,
+      isSignerMetadataOutdated,
+      updateSignerMetadata,
+      skipSignerMetadataUpdate,
+    ]
   )
 
   if (error) {

+ 9 - 3
packages/atlas/src/providers/user/user.store.ts

@@ -7,7 +7,7 @@ export type UserStoreState = ActiveUserState & {
   walletAccounts: SignerWalletAccount[]
   walletStatus: SignerWalletStatus
   lastUsedWalletName: string | null
-
+  lastChainMetadataVersion: [string, number] | null // [genesisHash, number]
   signInModalOpen: boolean
 }
 
@@ -21,6 +21,7 @@ export type UserStoreActions = {
   setWalletStatus: (status: SignerWalletStatus) => void
 
   setSignInModalOpen: (isOpen: boolean) => void
+  setLastChainMetadataVersion: (genesisHash: string, version: number) => void
 }
 
 export const useUserStore = createStore<UserStoreState, UserStoreActions>(
@@ -34,7 +35,7 @@ export const useUserStore = createStore<UserStoreState, UserStoreActions>(
       walletAccounts: [],
       walletStatus: 'unknown',
       lastUsedWalletName: null,
-
+      lastChainMetadataVersion: null,
       signInModalOpen: false,
     },
     actionsFactory: (set) => ({
@@ -85,13 +86,18 @@ export const useUserStore = createStore<UserStoreState, UserStoreActions>(
           state.signInModalOpen = isOpen
         })
       },
+      setLastChainMetadataVersion: (genesisHash, version) => {
+        set((state) => {
+          state.lastChainMetadataVersion = [genesisHash, version]
+        })
+      },
     }),
   },
   {
     persist: {
       key: 'activeUser',
       version: 0,
-      whitelist: ['accountId', 'memberId', 'channelId', 'lastUsedWalletName'],
+      whitelist: ['accountId', 'memberId', 'channelId', 'lastUsedWalletName', 'lastChainMetadataVersion'],
       migrate: (oldState) => {
         return oldState
       },

+ 3 - 2
packages/atlas/src/providers/user/user.types.ts

@@ -17,10 +17,11 @@ export type UserContextValue = {
   memberships: Membership[]
   membershipsLoading: boolean
   activeMembership: Membership | null
+  isSignerMetadataOutdated: boolean
   activeChannel: Membership['channels'][number] | null
-
   isAuthLoading: boolean
-
   signIn: (walletName?: string, mobileCallback?: ({ onConfirm }: { onConfirm: () => void }) => void) => Promise<boolean>
+  updateSignerMetadata: () => Promise<boolean>
+  skipSignerMetadataUpdate: () => Promise<void>
   refetchUserMemberships: ReturnType<typeof useMemberships>['refetch']
 }