From 04de54955f0a9f05321a5f59d31c829fcef0896a Mon Sep 17 00:00:00 2001 From: Anthony Khoury Date: Fri, 22 May 2026 18:26:06 +0300 Subject: [PATCH 1/3] fix(console): removed dapps, removed ethers, and enhancements (#144) * fix(console): deprecated dapps, removed ethers, and enhancements * fix: yarn * fix: ci build * fix: removed fully mapps * fix: yarn * fix: vercel build --- .../sections/code/CodeSection/index.tsx | 2 +- apps/console/tsconfig.json | 14 +- package.json | 3 +- packages/@justaname.id/react/package.json | 3 +- .../react/src/lib/helpers/ethersCompat.ts | 51 --- .../lib/helpers/resolveDefaultChain.spec.ts | 24 ++ .../src/lib/helpers/resolveDefaultChain.ts | 18 + .../react/src/lib/hooks/index.ts | 1 - .../react/src/lib/hooks/mApp/index.ts | 5 - .../lib/hooks/mApp/useAddMAppPermission.ts | 72 ---- .../src/lib/hooks/mApp/useCanEnableMApps.ts | 50 --- .../src/lib/hooks/mApp/useEnabledMApps.ts | 70 ---- .../src/lib/hooks/mApp/useIsMAppEnabled.ts | 79 ----- .../lib/hooks/mApp/useRevokeMAppPermission.ts | 70 ---- .../useSetNameHashJustaNameResolver.ts | 3 +- .../src/lib/hooks/subname/useAddSubname.ts | 18 +- .../src/lib/providers/JustaNameProvider.tsx | 28 +- packages/@justaname.id/sdk/package.json | 3 +- .../@justaname.id/sdk/src/lib/api/rest.ts | 11 + .../sdk/src/lib/api/routes/index.ts | 26 -- .../sdk/src/lib/api/routes/mapp.ts | 22 -- .../sdk/src/lib/api/routes/siwe.ts | 15 - .../sdk/src/lib/features/index.ts | 3 +- .../sdk/src/lib/features/mApps/index.ts | 170 --------- .../sdk/src/lib/features/sign-in/index.ts | 26 +- .../lib/features/subname-challenge/index.ts | 14 +- .../sdk/src/lib/justaname/index.ts | 96 +----- .../@justaname.id/sdk/src/lib/types/index.ts | 1 - .../src/lib/types/justaname/configuration.ts | 4 +- .../lib/types/mApps/add-mApp-permission.ts | 10 - .../src/lib/types/mApps/append-mApp-field.ts | 14 - .../sdk/src/lib/types/mApps/index.ts | 4 - .../sdk/src/lib/types/mApps/mApp.ts | 3 - .../lib/types/mApps/revoke-mApp-permission.ts | 10 - .../siwe/add-mApp-permission-challenge.ts | 70 ---- .../types/siwe/append-mApp-field-challenge.ts | 70 ---- .../sdk/src/lib/types/siwe/index.ts | 3 - .../siwe/revoke-mApp-permission-challenge.ts | 64 ---- .../sdk/src/lib/utils/ethersCompat.ts | 51 --- .../src/test/features/sign-in/sign-in.spec.ts | 34 +- .../subname-challenge.spec.ts | 8 +- .../src/test/integration/justaname.spec.ts | 179 ++-------- packages/@justaname.id/siwens/package.json | 7 +- .../siwens/src/lib/siwens/siwens.ts | 37 +- .../siwens/src/lib/utils/ethersCompat.ts | 51 --- .../siwens/src/test/siwens.spec.ts | 49 ++- .../components/EmailCredentialItem/index.tsx | 3 - .../src/lib/dialogs/EmailDialog/index.tsx | 46 +-- .../lib/dialogs/JustVerifiedDialog/index.tsx | 47 +-- .../plugin/src/lib/plugins/index.tsx | 3 - .../src/lib/icons/components/general/Mapp.tsx | 31 -- .../src/lib/icons/components/general/index.ts | 3 - .../lib/components/JustWeb3Button/index.tsx | 58 +--- .../lib/dialogs/AuthorizeMAppDialog/index.tsx | 287 ---------------- .../src/lib/dialogs/MAppsDialog/index.tsx | 268 --------------- .../lib/dialogs/RevokeMAppDialog/index.tsx | 251 -------------- .../@justweb3/widget/src/lib/dialogs/index.ts | 1 - .../@justweb3/widget/src/lib/plugins/index.ts | 25 +- .../lib/providers/JustWeb3Provider/index.tsx | 46 +-- .../src/lib/providers/MAppProvider/index.tsx | 322 ------------------ .../lib/providers/PluginProvider/index.tsx | 128 +------ .../widget/src/lib/providers/index.ts | 1 - .../widget/src/lib/types/config/index.ts | 1 - .../widget/src/stories/multichain.stories.tsx | 1 - .../widget/src/stories/signin.stories.tsx | 1 - packages/@justweb3/xmtp-plugin/package.json | 1 + .../src/lib/hooks/useClient/index.ts | 22 +- .../src/lib/hooks/useEthersSigner/index.ts | 59 ++-- packages/siwens/package.json | 7 +- yarn.lock | 151 +++----- 70 files changed, 419 insertions(+), 2910 deletions(-) delete mode 100644 packages/@justaname.id/react/src/lib/helpers/ethersCompat.ts create mode 100644 packages/@justaname.id/react/src/lib/helpers/resolveDefaultChain.spec.ts create mode 100644 packages/@justaname.id/react/src/lib/helpers/resolveDefaultChain.ts delete mode 100644 packages/@justaname.id/react/src/lib/hooks/mApp/index.ts delete mode 100644 packages/@justaname.id/react/src/lib/hooks/mApp/useAddMAppPermission.ts delete mode 100644 packages/@justaname.id/react/src/lib/hooks/mApp/useCanEnableMApps.ts delete mode 100644 packages/@justaname.id/react/src/lib/hooks/mApp/useEnabledMApps.ts delete mode 100644 packages/@justaname.id/react/src/lib/hooks/mApp/useIsMAppEnabled.ts delete mode 100644 packages/@justaname.id/react/src/lib/hooks/mApp/useRevokeMAppPermission.ts delete mode 100644 packages/@justaname.id/sdk/src/lib/api/routes/mapp.ts delete mode 100644 packages/@justaname.id/sdk/src/lib/features/mApps/index.ts delete mode 100644 packages/@justaname.id/sdk/src/lib/types/mApps/add-mApp-permission.ts delete mode 100644 packages/@justaname.id/sdk/src/lib/types/mApps/append-mApp-field.ts delete mode 100644 packages/@justaname.id/sdk/src/lib/types/mApps/index.ts delete mode 100644 packages/@justaname.id/sdk/src/lib/types/mApps/mApp.ts delete mode 100644 packages/@justaname.id/sdk/src/lib/types/mApps/revoke-mApp-permission.ts delete mode 100644 packages/@justaname.id/sdk/src/lib/types/siwe/add-mApp-permission-challenge.ts delete mode 100644 packages/@justaname.id/sdk/src/lib/types/siwe/append-mApp-field-challenge.ts delete mode 100644 packages/@justaname.id/sdk/src/lib/types/siwe/revoke-mApp-permission-challenge.ts delete mode 100644 packages/@justaname.id/sdk/src/lib/utils/ethersCompat.ts delete mode 100644 packages/@justaname.id/siwens/src/lib/utils/ethersCompat.ts delete mode 100644 packages/@justweb3/ui/src/lib/icons/components/general/Mapp.tsx delete mode 100644 packages/@justweb3/widget/src/lib/dialogs/AuthorizeMAppDialog/index.tsx delete mode 100644 packages/@justweb3/widget/src/lib/dialogs/MAppsDialog/index.tsx delete mode 100644 packages/@justweb3/widget/src/lib/dialogs/RevokeMAppDialog/index.tsx delete mode 100644 packages/@justweb3/widget/src/lib/providers/MAppProvider/index.tsx diff --git a/apps/console/src/components/sections/code/CodeSection/index.tsx b/apps/console/src/components/sections/code/CodeSection/index.tsx index 82ca795f..4ca36802 100644 --- a/apps/console/src/components/sections/code/CodeSection/index.tsx +++ b/apps/console/src/components/sections/code/CodeSection/index.tsx @@ -190,7 +190,7 @@ export default App;`.trim(); } ${poapPluginEnabled ? '@justweb3/poap-plugin' : ''} ${ efpPluginEnabled ? '@justweb3/efp-plugin' : '' } ${dentityPluginEnabled ? '@justweb3/dentity-plugin' : ''} - @justweb3/widget viem wagmi @rainbow-me/rainbowkit @tanstack/react-query ethers`; + @justweb3/widget viem wagmi @rainbow-me/rainbowkit @tanstack/react-query`; }, [ dentityPluginEnabled, efpPluginEnabled, diff --git a/apps/console/tsconfig.json b/apps/console/tsconfig.json index 5aabca4b..7845c4a4 100644 --- a/apps/console/tsconfig.json +++ b/apps/console/tsconfig.json @@ -11,6 +11,12 @@ "resolveJsonModule": true, "isolatedModules": true, "incremental": true, + "target": "ESNext", + "downlevelIteration": true, + "skipLibCheck": true, + "composite": false, + "module": "ESNext", + "moduleResolution": "Bundler", "plugins": [ { "name": "next" @@ -41,5 +47,11 @@ ".next/types/**/*.ts", "../../dist/dist/apps/console/types/**/*.ts" ], - "exclude": ["node_modules", "jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] + "exclude": [ + "node_modules", + "**/node_modules/**", + "jest.config.ts", + "**/*.spec.ts", + "**/*.test.ts" + ] } diff --git a/package.json b/package.json index c333d219..d488bb73 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "cropperjs": "1.6.2", "dotenv": "16.4.5", "embla-carousel-react": "8.3.0", - "ethers": "6.11.1", "express": "4.18.1", "express-session": "1.18.0", "input-otp": "1.2.4", @@ -83,7 +82,7 @@ "tailwindcss-animate": "1.0.7", "tslib": "2.3.0", "vaul": "1.1.1", - "viem": "^2.35.0", + "viem": "^2.48.0", "vite-plugin-dts": "3.7.3", "wagmi": "2.14.16" }, diff --git a/packages/@justaname.id/react/package.json b/packages/@justaname.id/react/package.json index 4a35e6e8..7eab989b 100644 --- a/packages/@justaname.id/react/package.json +++ b/packages/@justaname.id/react/package.json @@ -9,9 +9,8 @@ }, "peerDependencies": { "@tanstack/react-query": "^5.x", - "ethers": "^5.6.8 || ^6.0.8", "react": ">=17", - "viem": "2.x", + "viem": "^2.48.0", "wagmi": "2.x" }, "devDependencies": { diff --git a/packages/@justaname.id/react/src/lib/helpers/ethersCompat.ts b/packages/@justaname.id/react/src/lib/helpers/ethersCompat.ts deleted file mode 100644 index 682e9f25..00000000 --- a/packages/@justaname.id/react/src/lib/helpers/ethersCompat.ts +++ /dev/null @@ -1,51 +0,0 @@ -// inspired by spruceid siwe: https://github.com/spruceid/siwe/blob/main/packages/siwe/lib/ethersCompat.ts - -import { ethers } from 'ethers'; - -// @ts-expect-error -- compatibility hack -type ProviderV5 = ethers.providers.Provider; -type ProviderV6 = ethers.Provider; -// @ts-expect-error -- compatibility hack -type JsonRpcProviderV5 = ethers.providers.JsonRpcProvider; -type JsonRpcProviderV6 = ethers.JsonRpcProvider; - -export type Provider = ProviderV6 extends undefined ? ProviderV5 : ProviderV6; -export type JsonRpcProvider = JsonRpcProviderV6 extends undefined - ? JsonRpcProviderV5 - : JsonRpcProviderV6; - -interface EthersCompat { - namehash?: (name: string) => string; - getAddress?: (address: string) => string; - JsonRpcProvider?: new (...args: any[]) => JsonRpcProvider; - utils: { - namehash: (name: string) => string; - getAddress: (address: string) => string; - }; - providers: { - JsonRpcProvider: new (...args: any[]) => JsonRpcProvider; - }; -} - -const ethersCompat = ethers as unknown as EthersCompat; - -export const getJsonRpcProvider = ( - providerUrl?: string, - chainId?: number -): JsonRpcProvider => { - if ('JsonRpcProvider' in ethersCompat) { - return new ethersCompat.JsonRpcProvider!(providerUrl, chainId); - } else { - return new ethersCompat.providers.JsonRpcProvider(providerUrl, chainId); - } -}; - -export const namehash: (name: string) => string = - 'namehash' in ethersCompat - ? ethersCompat.namehash! - : ethersCompat.utils.namehash; - -export const getAddress: (address: string) => string = - 'getAddress' in ethersCompat - ? ethersCompat.getAddress! - : ethersCompat.utils.getAddress; diff --git a/packages/@justaname.id/react/src/lib/helpers/resolveDefaultChain.spec.ts b/packages/@justaname.id/react/src/lib/helpers/resolveDefaultChain.spec.ts new file mode 100644 index 00000000..9ae8f2f1 --- /dev/null +++ b/packages/@justaname.id/react/src/lib/helpers/resolveDefaultChain.spec.ts @@ -0,0 +1,24 @@ +import { resolveDefaultChain } from './resolveDefaultChain'; + +describe('resolveDefaultChain', () => { + it('returns undefined when chainId is undefined (no wallet connected)', () => { + // Regression: previously `!chainId === undefined` was always false, causing + // an undefined chainId to silently fall through to mainnet — which then + // locked the SDK into mainnet config when the user intended Sepolia. + expect(resolveDefaultChain(undefined)).toBeUndefined(); + }); + + it('passes through mainnet (1)', () => { + expect(resolveDefaultChain(1)).toBe(1); + }); + + it('passes through Sepolia (11155111)', () => { + expect(resolveDefaultChain(11155111)).toBe(11155111); + }); + + it('falls back to mainnet for unsupported chains', () => { + expect(resolveDefaultChain(137)).toBe(1); + expect(resolveDefaultChain(10)).toBe(1); + expect(resolveDefaultChain(42161)).toBe(1); + }); +}); diff --git a/packages/@justaname.id/react/src/lib/helpers/resolveDefaultChain.ts b/packages/@justaname.id/react/src/lib/helpers/resolveDefaultChain.ts new file mode 100644 index 00000000..9f1ae1f7 --- /dev/null +++ b/packages/@justaname.id/react/src/lib/helpers/resolveDefaultChain.ts @@ -0,0 +1,18 @@ +import { ChainId } from '@justaname.id/sdk'; + +/** + * Resolves the wagmi-provided chainId to a supported JustaName ChainId. + * + * - Returns `undefined` when no chain is available (wallet not connected yet); + * callers should defer SDK initialization until a chain resolves. + * - Returns `1` (mainnet) for any chainId that is not one of the two + * officially supported networks (1, 11155111). + * - Otherwise passes the chainId through unchanged. + */ +export const resolveDefaultChain = ( + chainId: number | undefined +): ChainId | undefined => { + if (chainId === undefined) return undefined; + if (chainId !== 1 && chainId !== 11155111) return 1; + return chainId; +}; diff --git a/packages/@justaname.id/react/src/lib/hooks/index.ts b/packages/@justaname.id/react/src/lib/hooks/index.ts index 2bca5d6f..d91734d8 100644 --- a/packages/@justaname.id/react/src/lib/hooks/index.ts +++ b/packages/@justaname.id/react/src/lib/hooks/index.ts @@ -1,6 +1,5 @@ export * from './account'; export * from './ens'; -export * from './mApp'; export * from './uploadMedia'; export * from './records'; export * from './resolver'; diff --git a/packages/@justaname.id/react/src/lib/hooks/mApp/index.ts b/packages/@justaname.id/react/src/lib/hooks/mApp/index.ts deleted file mode 100644 index 65858aa0..00000000 --- a/packages/@justaname.id/react/src/lib/hooks/mApp/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './useAddMAppPermission'; -export * from './useRevokeMAppPermission'; -export * from './useIsMAppEnabled'; -export * from './useCanEnableMApps'; -export * from './useEnabledMApps'; \ No newline at end of file diff --git a/packages/@justaname.id/react/src/lib/hooks/mApp/useAddMAppPermission.ts b/packages/@justaname.id/react/src/lib/hooks/mApp/useAddMAppPermission.ts deleted file mode 100644 index f629a833..00000000 --- a/packages/@justaname.id/react/src/lib/hooks/mApp/useAddMAppPermission.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { UseMutateAsyncFunction, useMutation } from '@tanstack/react-query'; -import { useJustaName } from '../../providers'; -import { ChainId, RequestAddMAppPermissionChallengeRoute, AddMAppPermissionRoute } from '@justaname.id/sdk'; -import { useSignMessage } from 'wagmi'; -import { useAccountSubnames, useMountedAccount } from '../account'; -import { useRecords } from '../records'; -import { useMemo } from 'react'; - -export interface UseAddMAppPermissionFunctionParams extends Omit { - mApp?: string -} - -export interface UseAddMAppPermissionParams extends Omit { - mApp: string - chainId?: ChainId -} - -export interface UseRequestAddMAppPermission { - addMAppPermission: UseMutateAsyncFunction, - isAddMAppPermissionPending: boolean; -} - - -export const useAddMAppPermission = (params: UseAddMAppPermissionParams): UseRequestAddMAppPermission => { - const { justaname, chainId } = useJustaName() - const { signMessageAsync } = useSignMessage() - const { address} = useMountedAccount() - const { getRecords } = useRecords() - const _chainId = useMemo(() => params.chainId || chainId, [params.chainId, chainId]) - - const { refetchAccountSubnames } = useAccountSubnames() - const mutate = useMutation({ - mutationFn: async ( - _params: UseAddMAppPermissionFunctionParams - ) => { - if (!address) { - throw new Error('Wallet not connected') - } - - const challengeResponse = await justaname.mApps.requestAddMAppPermissionChallenge({ - subname: _params.subname, - address: address, - mApp: _params?.mApp || params.mApp, - chainId: _params?.chainId || _chainId - }) - - const signature = await signMessageAsync({ - message: challengeResponse.challenge, - account: address - }) - - const response = await justaname.mApps.addMAppPermission({ - message: challengeResponse.challenge, - address: address, - signature, - }) - - refetchAccountSubnames() - - getRecords({ - ens: _params.subname, - chainId: _chainId, - }, true) - return response - } - }) - - return { - addMAppPermission: mutate.mutateAsync, - isAddMAppPermissionPending: mutate.isPending - } -} \ No newline at end of file diff --git a/packages/@justaname.id/react/src/lib/hooks/mApp/useCanEnableMApps.ts b/packages/@justaname.id/react/src/lib/hooks/mApp/useCanEnableMApps.ts deleted file mode 100644 index 3885e8bd..00000000 --- a/packages/@justaname.id/react/src/lib/hooks/mApp/useCanEnableMApps.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { ChainId } from '@justaname.id/sdk'; -import { useQuery } from '@tanstack/react-query'; -import { useJustaName } from '../../providers'; -import { useRecords } from '../records'; -import { useMemo } from 'react'; -import { defaultOptions } from '../../query'; - -export const buildCanEnableMAppsKey = ( - ens: string, - chainId: ChainId | undefined -) => ['CAN_ENABLE_MAPPS', ens, chainId]; - -export interface UseCanEnableMAppsParams { - ens: string; - chainId?: ChainId; -} - -export interface UseCanEnableMAppsResult { - canEnableMApps: boolean | undefined; - isCanEnableMAppsPending: boolean; - refetchCanEnableMApps: () => void; -} - -export const useCanEnableMApps = ( - params: UseCanEnableMAppsParams -): UseCanEnableMAppsResult => { - const { chainId } = useJustaName(); - const _chainId = useMemo( - () => params.chainId || chainId, - [params.chainId, chainId] - ); - const { records } = useRecords({ - ens: params.ens, - chainId: _chainId, - }); - const query = useQuery({ - ...defaultOptions, - queryKey: buildCanEnableMAppsKey(params.ens, _chainId), - queryFn: () => { - return records?.isJAN; - }, - enabled: Boolean(params.ens) && Boolean(_chainId) && Boolean(records), - }); - - return { - canEnableMApps: query.data, - refetchCanEnableMApps: query.refetch, - isCanEnableMAppsPending: query.isPending, - }; -}; diff --git a/packages/@justaname.id/react/src/lib/hooks/mApp/useEnabledMApps.ts b/packages/@justaname.id/react/src/lib/hooks/mApp/useEnabledMApps.ts deleted file mode 100644 index e029eb95..00000000 --- a/packages/@justaname.id/react/src/lib/hooks/mApp/useEnabledMApps.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ChainId } from '@justaname.id/sdk'; -import { useQuery } from '@tanstack/react-query'; -import { useJustaName } from '../../providers'; -import { useRecords } from '../records'; -import { useEffect, useMemo } from 'react'; -import { defaultOptions } from '../../query'; - -export const buildEnabledMAppsKey = ( - ens: string, - chainId: ChainId | undefined -) => ['ENABLED_MAPPS', ens, chainId]; - -export interface UseEnabledMAppsParams { - ens: string; - chainId?: ChainId; - providerUrl?: string; -} - -export interface UseEnabledMAppsResult { - enabledMApps: string[] | undefined; - refetchEnabledMApps: () => void; - isMAppEnabledPending: boolean; -} - -export const useEnabledMApps = ( - params: UseEnabledMAppsParams -): UseEnabledMAppsResult => { - const { justaname, chainId } = useJustaName(); - const _chainId = useMemo( - () => params.chainId || chainId, - [params.chainId, chainId] - ); - const { records } = useRecords({ - ens: params.ens, - chainId: _chainId, - }); - - const query = useQuery({ - ...defaultOptions, - queryKey: buildEnabledMAppsKey(params.ens, _chainId), - queryFn: async () => { - if (!records) { - return; - } - if (!records.isJAN) { - return false; - } - const mAppField = records.records.texts.find( - (text) => text.key === 'mApps' - ); - return mAppField ? JSON.parse(mAppField.value).mApps : []; - }, - enabled: - Boolean(params.ens) && - Boolean(justaname) && - Boolean(params.ens.length > 0) && - Boolean(_chainId) && - Boolean(records), - }); - - useEffect(() => { - if (records) query.refetch(); - }, [records]); - - return { - enabledMApps: query.data, - refetchEnabledMApps: query.refetch, - isMAppEnabledPending: query.isPending, - }; -}; diff --git a/packages/@justaname.id/react/src/lib/hooks/mApp/useIsMAppEnabled.ts b/packages/@justaname.id/react/src/lib/hooks/mApp/useIsMAppEnabled.ts deleted file mode 100644 index a81b3a78..00000000 --- a/packages/@justaname.id/react/src/lib/hooks/mApp/useIsMAppEnabled.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { ChainId } from '@justaname.id/sdk'; -import { useQuery } from '@tanstack/react-query'; -import { useJustaName } from '../../providers'; -import { useRecords } from '../records'; -import { useEffect, useMemo } from 'react'; -import { defaultOptions } from '../../query'; - -export const buildIsMAppEnabledKey = ( - ens: string, - mApp: string, - chainId: ChainId | undefined -) => ['IS_MAPP_ENABLED', ens, mApp, chainId]; - -export interface UseIsMAppEnabledParams { - ens: string; - mApp: string; - chainId?: ChainId; -} - -export interface UseIsMAppEnabledResult { - isMAppEnabled: boolean | undefined; - isMAppEnabledPending: boolean; - refetchIsMAppEnabled: () => void; -} - -export const useIsMAppEnabled = ( - params: UseIsMAppEnabledParams -): UseIsMAppEnabledResult => { - const { justaname, chainId } = useJustaName(); - const _chainId = useMemo( - () => params.chainId || chainId, - [params.chainId, chainId] - ); - const { records } = useRecords({ - ens: params.ens, - chainId: _chainId, - }); - - const query = useQuery({ - ...defaultOptions, - queryKey: buildIsMAppEnabledKey(params.ens, params.mApp, _chainId), - queryFn: async () => { - if (!records) { - return false; - } - if (!records.isJAN) { - return false; - } - const mAppField = records.records.texts.find( - (text) => text.key === 'mApps' - ); - if (!mAppField) { - return false; - } - const mAppFieldValue = JSON.parse(mAppField.value); - if (!mAppFieldValue) { - return false; - } - return mAppFieldValue.mApps.includes(params?.mApp); - }, - enabled: - Boolean(params.ens) && - Boolean(justaname) && - params.ens.length > 0 && - params?.mApp?.length > 0 && - Boolean(_chainId) && - Boolean(records), - }); - - useEffect(() => { - query.refetch(); - }, [records]); - - return { - isMAppEnabled: query.data, - refetchIsMAppEnabled: query.refetch, - isMAppEnabledPending: query.isPending, - }; -}; diff --git a/packages/@justaname.id/react/src/lib/hooks/mApp/useRevokeMAppPermission.ts b/packages/@justaname.id/react/src/lib/hooks/mApp/useRevokeMAppPermission.ts deleted file mode 100644 index 3813b542..00000000 --- a/packages/@justaname.id/react/src/lib/hooks/mApp/useRevokeMAppPermission.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { UseMutateAsyncFunction, useMutation } from '@tanstack/react-query'; -import { useJustaName } from '../../providers'; -import { ChainId, RevokeMAppPermissionRoute } from '@justaname.id/sdk'; -import { useSignMessage } from 'wagmi'; -import { useAccountSubnames, useMountedAccount } from '../account'; -import { useRecords } from '../records'; -import { useMemo } from 'react'; - -export interface UseRequestRevokeMAppPermissionResult { - revokeMAppPermission: UseMutateAsyncFunction; - isRevokeMAppPermissionPending: boolean; -} - -export interface UseRevokeMAppPermissionFunctionParams { - ens: string -} - -export interface UseRevokeMAppPermissionParams { - mApp: string, - chainId?: ChainId, - providerUrl?: string -} - -export const useRevokeMAppPermission = (params: UseRevokeMAppPermissionParams): UseRequestRevokeMAppPermissionResult => { - const { justaname, chainId } = useJustaName() - const _chainId = useMemo(() => params.chainId || chainId, [params.chainId, chainId]) - const { signMessageAsync } = useSignMessage() - const { address} = useMountedAccount() - const { refetchAccountSubnames } = useAccountSubnames() - const { getRecords } = useRecords() - const mutate = useMutation({ - mutationFn: async ( - _params:UseRevokeMAppPermissionFunctionParams - ) => { - if (!address) { - throw new Error('Wallet not connected') - } - const challengeResponse = await justaname.mApps.requestRevokeMAppPermissionChallenge({ - subname: _params.ens, - address: address, - mApp: params.mApp, - chainId: _chainId - }) - - const signature = await signMessageAsync({ - message: challengeResponse.challenge, - account: address - }) - - const response = await justaname.mApps.revokeMAppPermission({ - message: challengeResponse.challenge, - address: address, - signature - }) - - refetchAccountSubnames() - getRecords({ - ens: _params.ens, - chainId: _chainId, - }, true) - - return response - } - }) - - return { - revokeMAppPermission: mutate.mutateAsync, - isRevokeMAppPermissionPending: mutate.isPending - } -} \ No newline at end of file diff --git a/packages/@justaname.id/react/src/lib/hooks/resolver/useSetNameHashJustaNameResolver.ts b/packages/@justaname.id/react/src/lib/hooks/resolver/useSetNameHashJustaNameResolver.ts index f5ddaf4f..14300a9d 100644 --- a/packages/@justaname.id/react/src/lib/hooks/resolver/useSetNameHashJustaNameResolver.ts +++ b/packages/@justaname.id/react/src/lib/hooks/resolver/useSetNameHashJustaNameResolver.ts @@ -10,7 +10,8 @@ import { } from 'wagmi'; import { useOffchainResolvers } from '../offchainResolver/useOffchainResolvers'; import { useMountedAccount } from '../account/useMountedAccount'; -import { getAddress, namehash } from '../../helpers/ethersCompat'; +import { getAddress } from 'viem'; +import { namehash } from 'viem/ens'; const ZeroAddress = '0x0000000000000000000000000000000000000000'; diff --git a/packages/@justaname.id/react/src/lib/hooks/subname/useAddSubname.ts b/packages/@justaname.id/react/src/lib/hooks/subname/useAddSubname.ts index 84acd5b6..8f45c37e 100644 --- a/packages/@justaname.id/react/src/lib/hooks/subname/useAddSubname.ts +++ b/packages/@justaname.id/react/src/lib/hooks/subname/useAddSubname.ts @@ -5,7 +5,7 @@ import { useJustaName, useSubnameSignature } from '../../providers'; import { useMountedAccount } from '../account/useMountedAccount'; import { sanitizeRecords, SubnameAddRoute } from '@justaname.id/sdk'; import { useAccountSubnames } from '../account/useAccountSubnames'; -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { Records } from '../../types'; export type UseAddSubnameFunctionParams = SubnameAddRoute['params']; @@ -28,7 +28,8 @@ export interface UseAddSubnameResult { export const useAddSubname = ( params?: UseAddSubnameParams ): UseAddSubnameResult => { - const { justaname, backendUrl, routes, chainId, ensDomains } = useJustaName(); + const { justaname, backendUrl, routes, chainId, ensDomains, dev } = + useJustaName(); const { address } = useMountedAccount(); const { getSignature } = useSubnameSignature(); const { refetchAccountSubnames } = useAccountSubnames(); @@ -42,6 +43,7 @@ export const useAddSubname = ( ensDomains.find((ensDomain) => ensDomain.chainId === _chainId)?.ensDomain, [params?.ensDomain, ensDomains, _chainId] ); + const _backendUrl = useMemo( () => params?.backendUrl || backendUrl, [params?.backendUrl, backendUrl] @@ -58,6 +60,18 @@ export const useAddSubname = ( params?.apiKey || ensDomains.find((ensDomain) => ensDomain.chainId === _chainId)?.apiKey; + useEffect(() => { + if (dev) { + // eslint-disable-next-line no-console + console.debug( + '[JustaName] useAddSubname resolved chainId:', + _chainId, + 'ensDomain:', + _ensDomain + ); + } + }, [dev, _chainId, _ensDomain]); + const mutate = useMutation({ mutationFn: async (_params: UseAddSubnameFunctionParams) => { if (!address) { diff --git a/packages/@justaname.id/react/src/lib/providers/JustaNameProvider.tsx b/packages/@justaname.id/react/src/lib/providers/JustaNameProvider.tsx index b3a690a3..8a506171 100644 --- a/packages/@justaname.id/react/src/lib/providers/JustaNameProvider.tsx +++ b/packages/@justaname.id/react/src/lib/providers/JustaNameProvider.tsx @@ -19,6 +19,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { defaultRoutes } from '../constants/default-routes'; import { useMountedAccount } from '../hooks/account/useMountedAccount'; import { useSignMessage } from 'wagmi'; +import { resolveDefaultChain } from '../helpers/resolveDefaultChain'; export type JustaNameConfigWithoutDefaultChainId = Omit< JustaNameConfig, @@ -32,7 +33,7 @@ export interface JustaNameContextProps justaname: JustaName; routes: typeof defaultRoutes; backendUrl: string; - selectedNetwork: NetworkWithProvider; + selectedNetwork: NetworkWithProvider | undefined; selectedEnsDomain: string | undefined; chainId: ChainId | undefined; } @@ -67,13 +68,20 @@ export const JustaNameProvider: FC = ({ }) => { const { chainId } = useMountedAccount(); - const defaultChain = useMemo(() => { - return !chainId === undefined - ? 1 - : chainId !== 1 && chainId !== 11155111 - ? 1 - : chainId; - }, [chainId]); + const defaultChain = useMemo(() => resolveDefaultChain(chainId), [chainId]); + + useEffect(() => { + if (initialConfig.dev) { + // eslint-disable-next-line no-console + console.debug( + '[JustaName] defaultChain resolved:', + defaultChain, + '(wagmi chainId:', + chainId, + ')' + ); + } + }, [defaultChain, chainId, initialConfig.dev]); // const [config, setConfig] = useState(initialConfig); const config: JustaNameProviderConfig = useMemo( @@ -104,10 +112,10 @@ export const JustaNameProvider: FC = ({ return JustaName.createNetworks(justanameConfig.networks); }, [justanameConfig.networks]); - const selectedNetwork = useMemo(() => { + const selectedNetwork = useMemo(() => { return configuredNetworks.find( (network) => network.chainId === defaultChain - ) as NetworkWithProvider; + ); }, [configuredNetworks, defaultChain]); return ( diff --git a/packages/@justaname.id/sdk/package.json b/packages/@justaname.id/sdk/package.json index f1c1be87..721d209b 100644 --- a/packages/@justaname.id/sdk/package.json +++ b/packages/@justaname.id/sdk/package.json @@ -10,9 +10,8 @@ "jest": "^29.4.1" }, "peerDependencies": { - "ethers": "^5.6.8 || ^6.0.8", "siwe": ">=2.0.0", - "viem": ">=2.35.0" + "viem": "^2.48.0" }, "exports": { "./package.json": "./dist/package.json", diff --git a/packages/@justaname.id/sdk/src/lib/api/rest.ts b/packages/@justaname.id/sdk/src/lib/api/rest.ts index 49cef6e0..f611090b 100644 --- a/packages/@justaname.id/sdk/src/lib/api/rest.ts +++ b/packages/@justaname.id/sdk/src/lib/api/rest.ts @@ -63,6 +63,17 @@ export const restCall = < } } + if (dev) { + // Log only the route and the non-sensitive top-level keys present on the + // payload. We avoid dumping the full body because it can contain SIWE + // messages, addresses, and other PII. + const payload = (request ?? {}) as Record; + const keys = Object.keys(payload); + const chainId = (payload as { chainId?: unknown }).chainId; + // eslint-disable-next-line no-console + console.debug('[JustaName]', method, Routes[route], { chainId, keys }); + } + return controlledAxiosPromise( justANameInstance(dev).request({ url: Routes[route], diff --git a/packages/@justaname.id/sdk/src/lib/api/routes/index.ts b/packages/@justaname.id/sdk/src/lib/api/routes/index.ts index 1c6060f9..9cd6868a 100644 --- a/packages/@justaname.id/sdk/src/lib/api/routes/index.ts +++ b/packages/@justaname.id/sdk/src/lib/api/routes/index.ts @@ -1,13 +1,7 @@ import { - AddMAppPermissionRoute, - AppendMAppFieldRoute, IsSubnameAvailableRoute, OffchainResolversGetAllRoute, - RequestAddMAppPermissionChallengeRoute, - RequestAppendMAppFieldChallengeRoute, RequestChallengeRoute, - RequestRevokeMAppPermissionChallengeRoute, - RevokeMAppPermissionRoute, SubnameAcceptRoute, SubnameAddRoute, SubnameGetAllByAddressRoute, @@ -39,17 +33,9 @@ import { UPDATE_SUBNAME_ROUTE, } from './subnames'; import { - SIWE_MAPP_ADD_PERMISSION_ROUTE, - SIWE_MAPP_APPEND_FIELD_ROUTE, - SIWE_MAPP_REVOKE_PERMISSION_ROUTE, SIWE_REQUEST_CHALLENGE_ROUTE, SIWE_VERIFY_MESSAGE_ROUTE, } from './siwe'; -import { - MAPP_ADD_PERMISSION_ROUTE, - MAPP_APPEND_FIELD_ROUTE, - MAPP_REVOKE_PERMISSION_ROUTE, -} from './mapp'; import { GET_ALL_OFFCHAIN_RESOLVERS_ROUTE } from './offchain-resolver'; import { GET_PRIMARY_NAME_BY_ADDRESS_ROUTE, @@ -63,12 +49,6 @@ import { export interface ROUTES { SIWE_VERIFY_MESSAGE_ROUTE: VerifyMessageRoute; SIWE_REQUEST_CHALLENGE_ROUTE: RequestChallengeRoute; - SIWE_MAPP_ADD_PERMISSION_ROUTE: RequestAddMAppPermissionChallengeRoute; - SIWE_MAPP_APPEND_FIELD_ROUTE: RequestAppendMAppFieldChallengeRoute; - SIWE_MAPP_REVOKE_PERMISSION_ROUTE: RequestRevokeMAppPermissionChallengeRoute; - MAPP_ADD_PERMISSION_ROUTE: AddMAppPermissionRoute; - MAPP_APPEND_FIELD_ROUTE: AppendMAppFieldRoute; - MAPP_REVOKE_PERMISSION_ROUTE: RevokeMAppPermissionRoute; ACCEPT_SUBNAME_ROUTE: SubnameAcceptRoute; RESERVE_SUBNAME_ROUTE: SubnameReserveRoute; ADD_SUBNAME_ROUTE: SubnameAddRoute; @@ -91,12 +71,6 @@ export interface ROUTES { export const Routes: Record = { SIWE_VERIFY_MESSAGE_ROUTE, SIWE_REQUEST_CHALLENGE_ROUTE, - SIWE_MAPP_ADD_PERMISSION_ROUTE, - SIWE_MAPP_APPEND_FIELD_ROUTE, - SIWE_MAPP_REVOKE_PERMISSION_ROUTE, - MAPP_ADD_PERMISSION_ROUTE, - MAPP_APPEND_FIELD_ROUTE, - MAPP_REVOKE_PERMISSION_ROUTE, ACCEPT_SUBNAME_ROUTE, RESERVE_SUBNAME_ROUTE, ADD_SUBNAME_ROUTE, diff --git a/packages/@justaname.id/sdk/src/lib/api/routes/mapp.ts b/packages/@justaname.id/sdk/src/lib/api/routes/mapp.ts deleted file mode 100644 index b5f37936..00000000 --- a/packages/@justaname.id/sdk/src/lib/api/routes/mapp.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { GLOBAL_PREFIX} from './prefix'; - -/** - * Prefix for all MAPP routes - */ -const MAPP_ROUTE = GLOBAL_PREFIX + '/ens/v1/mapp'; - -/** - * Routes for MAPP Add Permission - */ -export const MAPP_ADD_PERMISSION_ROUTE = MAPP_ROUTE + '/permission/add'; - -/** - * Routes for MAPP Append Field - */ -export const MAPP_APPEND_FIELD_ROUTE = MAPP_ROUTE + '/field/append'; - - -/** - * Routes for MAPP Revoke Permission - */ -export const MAPP_REVOKE_PERMISSION_ROUTE = MAPP_ROUTE + '/permission/revoke'; \ No newline at end of file diff --git a/packages/@justaname.id/sdk/src/lib/api/routes/siwe.ts b/packages/@justaname.id/sdk/src/lib/api/routes/siwe.ts index 3c233bcb..b5a2ae05 100644 --- a/packages/@justaname.id/sdk/src/lib/api/routes/siwe.ts +++ b/packages/@justaname.id/sdk/src/lib/api/routes/siwe.ts @@ -14,18 +14,3 @@ export const SIWE_REQUEST_CHALLENGE_ROUTE = SIWE_BASE_ROUTE + '/request-challeng * Routes for SIWE Verify Message */ export const SIWE_VERIFY_MESSAGE_ROUTE = SIWE_BASE_ROUTE + '/verify-message'; - -/** - * Routes for SIWE Request Challenge to Add MAPP Permission - */ -export const SIWE_MAPP_ADD_PERMISSION_ROUTE = SIWE_BASE_ROUTE + '/mapp/add-permission'; - -/** - * Routes for SIWE Request Challenge to Append Field - */ -export const SIWE_MAPP_APPEND_FIELD_ROUTE = SIWE_BASE_ROUTE + '/mapp/append-field'; - -/** - * Routes for SIWE Request Challenge to Revoke Permission - */ -export const SIWE_MAPP_REVOKE_PERMISSION_ROUTE = SIWE_BASE_ROUTE + '/mapp/revoke-permission'; \ No newline at end of file diff --git a/packages/@justaname.id/sdk/src/lib/features/index.ts b/packages/@justaname.id/sdk/src/lib/features/index.ts index b13eed13..a591573f 100644 --- a/packages/@justaname.id/sdk/src/lib/features/index.ts +++ b/packages/@justaname.id/sdk/src/lib/features/index.ts @@ -1,5 +1,4 @@ export * from './subname-challenge'; export * from './subnames'; export * from './offchain-resolvers'; -export * from './sign-in'; -export * from './mApps'; \ No newline at end of file +export * from './sign-in'; \ No newline at end of file diff --git a/packages/@justaname.id/sdk/src/lib/features/mApps/index.ts b/packages/@justaname.id/sdk/src/lib/features/mApps/index.ts deleted file mode 100644 index b8e3ac25..00000000 --- a/packages/@justaname.id/sdk/src/lib/features/mApps/index.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { - AddMAppPermissionRoute, - AppendMAppFieldRoute, - ChainId, - MApp, - NetworksWithProvider, - RequestAddMAppPermissionChallengeRoute, - RequestAppendMAppFieldChallengeRoute, - RequestRevokeMAppPermissionChallengeRoute, - RevokeMAppPermissionRoute, - SiweConfig -} from '../../types'; -import { assertRestCall } from '../../api/rest'; -import { Subnames } from '../subnames'; - -export interface MAppsParams { - siweConfig?: Omit; - chainId: ChainId; - networks: NetworksWithProvider - subnames: Subnames; - dev: boolean; -} - -export class MApps { - private readonly siweConfig?: Omit; - private readonly chainId: ChainId; - private readonly subnames: Subnames; - private readonly networks: NetworksWithProvider; - private readonly dev: boolean; - - constructor(params: MAppsParams) { - this.siweConfig = params.siweConfig; - this.chainId = params.chainId; - this.subnames = params.subnames; - this.networks = params.networks; - this.dev = params.dev; - } - - async checkIfMAppIsEnabled(params: { - mApp: string; - ens: string; - chainId?: ChainId; - }): Promise { - const chainId = params.chainId || this.chainId; - const network = this.networks.find((network) => network.chainId === chainId); - if (!network) { - throw new Error('Network not found'); - } - - - const mApp = params.mApp; - const ens = params.ens; - const records = await this.subnames.getRecords({ - ens, - chainId, - providerUrl: network.providerUrl - }); - - if (!records) { - return false; - } - - if (!records.isJAN) { - return false; - } - - const mAppField = records.records.texts.find((text) => text.key === 'mApps'); - - if (!mAppField) { - return false; - } - - const mAppFieldValue = JSON.parse(mAppField.value) as MApp; - - if (!mAppFieldValue) { - return false; - } - - return mAppFieldValue.mApps.includes(mApp); - } - - async canEnableMApps(params: { - ens: string; - chainId?: ChainId; - }): Promise { - const chainId = params.chainId || this.chainId; - const ens = params.ens; - const records = await this.subnames.getRecords({ - ens, - chainId - }); - - return records.isJAN; - } - - requestAddMAppPermissionChallenge(params: RequestAddMAppPermissionChallengeRoute['params']): Promise { - const { chainId, ttl, origin, domain,...rest } = params; - - const _chainId = chainId || this.chainId; - const _ttl = ttl || 120000; - const _origin = origin || this.siweConfig?.origin; - const _domain = domain || this.siweConfig?.domain; - return assertRestCall('SIWE_MAPP_ADD_PERMISSION_ROUTE', 'POST', { - ttl: _ttl, - chainId: _chainId, - origin: _origin, - domain: _domain, - ...rest - }, undefined, this.dev)(['ttl','chainId','origin','domain']) - } - - requestAppendMAppFieldChallenge(params: RequestAppendMAppFieldChallengeRoute['params']): Promise { - const { chainId, ttl, origin, domain,...rest } = params; - const _chainId = chainId || this.chainId; - const _ttl = ttl || 120000; - const _origin = origin || this.siweConfig?.origin; - const _domain = domain || this.siweConfig?.domain; - - return assertRestCall('SIWE_MAPP_APPEND_FIELD_ROUTE', 'POST', { - ttl: _ttl, - chainId: _chainId, - origin: _origin, - domain: _domain, - ...rest - }, undefined, this.dev)(['ttl','chainId','origin','domain']) - } - - requestRevokeMAppPermissionChallenge(params: RequestRevokeMAppPermissionChallengeRoute['params']): Promise { - const { chainId, ttl, origin, domain,...rest } = params; - const _chainId = chainId || this.chainId; - const _ttl = ttl || 120000; - const _origin = origin || this.siweConfig?.origin; - const _domain = domain || this.siweConfig?.domain; - - return assertRestCall('SIWE_MAPP_REVOKE_PERMISSION_ROUTE', 'POST', { - ttl: _ttl, - chainId: _chainId, - origin: _origin, - domain: _domain, - ...rest - }, undefined, this.dev)(['ttl','chainId','origin','domain']) - } - - addMAppPermission( - params: AddMAppPermissionRoute['params'], - ): Promise { - return assertRestCall('MAPP_ADD_PERMISSION_ROUTE', 'POST', { - ...params - }, undefined, this.dev)(['message','address','signature']) - } - - appendMAppField( - params: AppendMAppFieldRoute['params'], - headers: AppendMAppFieldRoute['headers'] - ): Promise { - return assertRestCall('MAPP_APPEND_FIELD_ROUTE', 'POST', { - ...params - }, { - ...headers - }, this.dev)(['subname','fields'],['xAddress','xMessage','xSignature']) - } - - revokeMAppPermission( - params: RevokeMAppPermissionRoute['params'] - ): Promise { - return assertRestCall('MAPP_REVOKE_PERMISSION_ROUTE', 'POST', { - ...params - }, undefined, this.dev)(['message','address','signature']) - } -} diff --git a/packages/@justaname.id/sdk/src/lib/features/sign-in/index.ts b/packages/@justaname.id/sdk/src/lib/features/sign-in/index.ts index 997da2f0..c7619201 100644 --- a/packages/@justaname.id/sdk/src/lib/features/sign-in/index.ts +++ b/packages/@justaname.id/sdk/src/lib/features/sign-in/index.ts @@ -10,6 +10,7 @@ import { OffchainResolvers } from '../offchain-resolvers'; import { RequestSignInParams, SignInFunctionParams } from '../../types/signin'; import { createPublicClient, http } from 'viem'; import { mainnet, sepolia } from 'viem/chains'; +import { normalize } from 'viem/ens'; export interface SignInResponse extends SiwensResponse { isJustaName: boolean; @@ -116,10 +117,16 @@ export class SignIn { domain: params.domain, }, { - provider: network.provider, + // Smart-contract (EIP-1271) verification is handled inside + // `verificationFallback` below using viem's `verifySiweMessage`. + // We no longer pass `provider` here because it must be an ethers + // `Provider`, and the SDK is now viem-only. verificationFallback: async (params, opts, message, EIP1271Promise) => { + // Use the chainId extracted from the SIWE message itself, not the + // SDK-default. Otherwise contract-wallet (EIP-1271) verification + // runs against the wrong chain when the message is cross-chain. const publicClient = createPublicClient({ - chain: this.chainId === 1 ? mainnet : sepolia, + chain: chainId === 1 ? mainnet : sepolia, transport: http(network.providerUrl), }); @@ -209,7 +216,13 @@ export class SignIn { } const [resolverAddress, resolvers] = await Promise.all([ - network.provider.getResolver(ens), + // Narrow the catch to "resolver not found" (the only expected miss). + // Network / RPC errors must bubble up so callers see a real failure + // rather than a misleading "ENS not registered" downstream. + network.provider.getEnsResolver({ name: normalize(ens) }).catch((e) => { + if (e?.name === 'EnsResolverNotFoundError') return undefined; + throw e; + }), this.offchainResolvers.getAllOffchainResolvers(), ]); @@ -221,11 +234,14 @@ export class SignIn { throw InvalidENSException.chainNotSupported(chainId.toString()); } - if (!resolverAddress?.address) { + if (!resolverAddress) { throw InvalidENSException.notRegisteredENS(ens); } - return currentOffchainResolver.resolverAddress === resolverAddress?.address; + return ( + currentOffchainResolver.resolverAddress.toLowerCase() === + resolverAddress.toLowerCase() + ); } } diff --git a/packages/@justaname.id/sdk/src/lib/features/subname-challenge/index.ts b/packages/@justaname.id/sdk/src/lib/features/subname-challenge/index.ts index f1aa62d0..5d065d22 100644 --- a/packages/@justaname.id/sdk/src/lib/features/subname-challenge/index.ts +++ b/packages/@justaname.id/sdk/src/lib/features/subname-challenge/index.ts @@ -115,8 +115,20 @@ export class SubnameChallenge { expirationTime, }); + const prepared = siweMessage.prepareMessage(); + + if (this.dev) { + // eslint-disable-next-line no-console + console.debug( + '[JustaName] SIWE challenge prepared (chainId:', + _chainId, + ')\n', + prepared + ); + } + return { - challenge: siweMessage.prepareMessage(), + challenge: prepared, }; } diff --git a/packages/@justaname.id/sdk/src/lib/justaname/index.ts b/packages/@justaname.id/sdk/src/lib/justaname/index.ts index ca6d2372..1a66ca52 100644 --- a/packages/@justaname.id/sdk/src/lib/justaname/index.ts +++ b/packages/@justaname.id/sdk/src/lib/justaname/index.ts @@ -7,7 +7,6 @@ import { NetworkWithProvider, } from '../types'; import { - MApps, OffchainResolvers, SignIn, SubnameChallenge, @@ -15,7 +14,13 @@ import { } from '../features'; import { InvalidConfigurationException } from '../errors/InvalidConfiguration.exception'; // import { providerUrlChainIdLoadingMap, providerUrlChainIdMap } from '../memory'; -import { getJsonRpcProvider } from '../utils/ethersCompat'; +import { createPublicClient, http, PublicClient } from 'viem'; +import { mainnet, sepolia } from 'viem/chains'; + +const buildPublicClient = (providerUrl: string, chainId: 1 | 11155111): PublicClient => { + const chain = chainId === 1 ? mainnet : sepolia; + return createPublicClient({ chain, transport: http(providerUrl) }); +}; /** * The main class for the JustaName SDK. @@ -68,26 +73,16 @@ export class JustaName { **/ signIn: SignIn; - /** - * The MApps feature. - * @public - * @type {MApps} - * @memberof JustaName - */ - mApps: MApps; - constructor( siwe: SubnameChallenge, subnames: Subnames, offchainResolvers: OffchainResolvers, - signIn: SignIn, - mApps: MApps + signIn: SignIn ) { this.siwe = siwe; this.subnames = subnames; this.offchainResolvers = offchainResolvers; this.signIn = signIn; - this.mApps = mApps; } static init(configuration: JustaNameConfig = {}): JustaName { @@ -146,20 +141,11 @@ export class JustaName { offchainResolvers, }); - const mApps = new MApps({ - siweConfig, - chainId: defaultChainId, - networks, - subnames, - dev, - }); - return new JustaName( subnameChallenge, subnames, offchainResolvers, - signIn, - mApps + signIn ); } @@ -167,11 +153,11 @@ export class JustaName { const defaultMainnetProviderUrl = 'https://cloudflare-eth.com'; const defaultTestnetProviderUrl = 'https://rpc.sepolia.org'; - const defaultMainnetProvider = getJsonRpcProvider( + const defaultMainnetProvider = buildPublicClient( defaultMainnetProviderUrl, 1 ); - const defaultTestnetProvider = getJsonRpcProvider( + const defaultTestnetProvider = buildPublicClient( defaultTestnetProviderUrl, 11155111 ); @@ -187,11 +173,6 @@ export class JustaName { provider: defaultTestnetProvider, providerUrl: defaultTestnetProviderUrl, }, - // { - // chainId: 31337 as ChainId, - // provider: getJsonRpcProvider('http://localhost:8545'), - // providerUrl: 'http://localhost:8545', - // }, ] as NetworksWithProvider; const baseNetworksConfig = baseNetworks.map((_network) => { @@ -199,7 +180,10 @@ export class JustaName { if (network && network?.providerUrl) { return { chainId: network.chainId, - provider: getJsonRpcProvider(network.providerUrl), + provider: buildPublicClient( + network.providerUrl, + network.chainId as 1 | 11155111 + ), providerUrl: network.providerUrl, }; } else { @@ -213,9 +197,6 @@ export class JustaName { const testnetNetwork = baseNetworksConfig.find( (n) => n.chainId === 11155111 ) as NetworkWithProvider<11155111>; - // const localNetwork = baseNetworksConfig.find( - // (n) => n.chainId === 31337 - // ) as NetworkWithProvider<31337>; if (!mainnetNetwork) { throw new InvalidConfigurationException('The mainnet network is missing'); } @@ -231,51 +212,4 @@ export class JustaName { // To be optimized for serverless and re added later // this.checkNetworks(configuration.networks); } - - // private static checkNetworks(networks: Networks): void { - // if (networks && networks.length > 0) { - // networks.reduce((acc, network) => { - // if (acc.includes(network.chainId)) { - // throw new InvalidConfigurationException('The chainId is duplicated'); - // } - // return [...acc, network.chainId]; - // }, [] as ChainId[]); - // - // networks.forEach((network) => { - // if (providerUrlChainIdLoadingMap.has(network.providerUrl)) { - // if (providerUrlChainIdLoadingMap.get(network.providerUrl)) { - // return; - // } - // } - // - // providerUrlChainIdLoadingMap.set(network.providerUrl, true); - // - // if (providerUrlChainIdMap.has(network.providerUrl)) { - // if ( - // providerUrlChainIdMap.get(network.providerUrl) !== network.chainId - // ) { - // throw new InvalidConfigurationException( - // 'The chainId does not match the chainId of the providerUrl' - // ); - // } else { - // return; - // } - // } - // - // const provider = getJsonRpcProvider(network.providerUrl); - // provider.getNetwork().then((_network) => { - // if (network.chainId.toString() !== _network.chainId.toString()) { - // throw new InvalidConfigurationException( - // 'The chainId does not match the chainId of the providerUrl' - // ); - // } - // - // providerUrlChainIdMap.set( - // network.providerUrl, - // parseInt(_network.chainId.toString()) - // ); - // }); - // }); - // } - // } } diff --git a/packages/@justaname.id/sdk/src/lib/types/index.ts b/packages/@justaname.id/sdk/src/lib/types/index.ts index 4deaba43..6de4f444 100644 --- a/packages/@justaname.id/sdk/src/lib/types/index.ts +++ b/packages/@justaname.id/sdk/src/lib/types/index.ts @@ -7,5 +7,4 @@ export * from './offchain-resolver'; export * from './primary-name'; export * from './siwe'; export * from './subnames'; -export * from './mApps'; export * from './signin'; diff --git a/packages/@justaname.id/sdk/src/lib/types/justaname/configuration.ts b/packages/@justaname.id/sdk/src/lib/types/justaname/configuration.ts index 85b12ad9..745168c9 100644 --- a/packages/@justaname.id/sdk/src/lib/types/justaname/configuration.ts +++ b/packages/@justaname.id/sdk/src/lib/types/justaname/configuration.ts @@ -1,9 +1,9 @@ -import { JsonRpcProvider } from '../../utils/ethersCompat'; +import type { PublicClient } from 'viem'; import { ChainId } from '../common'; export interface NetworkWithProvider extends Network { - provider: JsonRpcProvider; + provider: PublicClient; } export interface Network { diff --git a/packages/@justaname.id/sdk/src/lib/types/mApps/add-mApp-permission.ts b/packages/@justaname.id/sdk/src/lib/types/mApps/add-mApp-permission.ts deleted file mode 100644 index 8073dc05..00000000 --- a/packages/@justaname.id/sdk/src/lib/types/mApps/add-mApp-permission.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { IRequest, IRoute, SubnameResponse } from '../common'; - -export interface AddMAppPermissionRequest extends IRequest { - address: string; - signature?: string; - message: string; -} - -export interface AddMAppPermissionRoute - extends IRoute {} diff --git a/packages/@justaname.id/sdk/src/lib/types/mApps/append-mApp-field.ts b/packages/@justaname.id/sdk/src/lib/types/mApps/append-mApp-field.ts deleted file mode 100644 index 2a9bfb9f..00000000 --- a/packages/@justaname.id/sdk/src/lib/types/mApps/append-mApp-field.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { IRequest, IRoute, SubnameResponse } from '../common'; -import { SIWEHeaders } from '../headers'; - -export interface AppendMAppFieldsRequest { - key: string; - value: string; -} - -export interface AppendMAppFieldRequest extends IRequest{ - subname: string; - fields: AppendMAppFieldsRequest[]; -} - -export interface AppendMAppFieldRoute extends IRoute {} \ No newline at end of file diff --git a/packages/@justaname.id/sdk/src/lib/types/mApps/index.ts b/packages/@justaname.id/sdk/src/lib/types/mApps/index.ts deleted file mode 100644 index c90cffbf..00000000 --- a/packages/@justaname.id/sdk/src/lib/types/mApps/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './add-mApp-permission' -export * from './append-mApp-field' -export * from './revoke-mApp-permission' -export * from './mApp' \ No newline at end of file diff --git a/packages/@justaname.id/sdk/src/lib/types/mApps/mApp.ts b/packages/@justaname.id/sdk/src/lib/types/mApps/mApp.ts deleted file mode 100644 index e2ee2fbe..00000000 --- a/packages/@justaname.id/sdk/src/lib/types/mApps/mApp.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface MApp { - mApps: string[] -} \ No newline at end of file diff --git a/packages/@justaname.id/sdk/src/lib/types/mApps/revoke-mApp-permission.ts b/packages/@justaname.id/sdk/src/lib/types/mApps/revoke-mApp-permission.ts deleted file mode 100644 index c1145ab5..00000000 --- a/packages/@justaname.id/sdk/src/lib/types/mApps/revoke-mApp-permission.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { IRequest, IRoute, SubnameResponse } from '../common'; - -export interface RevokeMAppPermissionRequest extends IRequest { - address: string; - signature?: string; - message: string; -} - -export interface RevokeMAppPermissionRoute - extends IRoute {} diff --git a/packages/@justaname.id/sdk/src/lib/types/siwe/add-mApp-permission-challenge.ts b/packages/@justaname.id/sdk/src/lib/types/siwe/add-mApp-permission-challenge.ts deleted file mode 100644 index bd337f5b..00000000 --- a/packages/@justaname.id/sdk/src/lib/types/siwe/add-mApp-permission-challenge.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ChainId, EmptyHeaders, IRequest, IResponse, IRoute } from '../common'; - -/** - * Represents a request to challenge to add mApp permission. - * @interface RequestAddMAppPermissionChallengeRequest - * @public - */ - -export interface RequestAddMAppPermissionChallengeRequest extends IRequest { - - /** - * Represents the ENS domain - * @type {string} - */ - domain: string; - - /** - * Represents the ethereum address to be challenged. - * @type {string} - */ - address: string; - - /** - * Represents the origin of the request (e.g. the domain of the website). - * @type {string} - */ - origin: string; - - /** - * Represents the chainId of the blockchain to be used. - * @type {1 | 11155111} - */ - chainId: ChainId; - - /** - * Specifies the time-to-live (TTL) for a variable. - * default: 120000 ms, 2 minutes ( 2 * 60 * 1000 ) - * @type {number} - * @default 120000 - * @optional - */ - ttl?: number; - - /** - * Subname requesting the ABDC Permission - * @type {string} - */ - subname: string - - /** - * Subname requesting the MApps Permission - * @type {string} - */ - mApp: string -} - -/** - * Represents the response to a request to challenge a specific address using SIWE. - * @interface RequestAddMAppPermissionChallengeResponse - * @public - */ -export interface RequestAddMAppPermissionChallengeResponse extends IResponse{ - /** - * Represents the challenge to be signed by the user. - * @type {string} - */ - challenge: string; -} - -export interface RequestAddMAppPermissionChallengeRoute extends IRoute {} \ No newline at end of file diff --git a/packages/@justaname.id/sdk/src/lib/types/siwe/append-mApp-field-challenge.ts b/packages/@justaname.id/sdk/src/lib/types/siwe/append-mApp-field-challenge.ts deleted file mode 100644 index 17442633..00000000 --- a/packages/@justaname.id/sdk/src/lib/types/siwe/append-mApp-field-challenge.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ChainId, EmptyHeaders, IRequest, IResponse, IRoute } from '../common'; - -/** - * Represents a request to challenge to add mApp permission. - * @interface RequestAppendMAppFieldChallengeRequest - * @public - */ - -export interface RequestAppendMAppFieldChallengeRequest extends IRequest { - - /** - * Represents the ENS domain - * @type {string} - */ - domain: string; - - /** - * Represents the ethereum address to be challenged. - * @type {string} - */ - address: string; - - /** - * Represents the origin of the request (e.g. the domain of the website). - * @type {string} - */ - origin: string; - - /** - * Represents the chainId of the blockchain to be used. - * @type {1 | 11155111} - */ - chainId: ChainId; - - /** - * Specifies the time-to-live (TTL) for a variable. - * default: 120000 ms, 2 minutes ( 2 * 60 * 1000 ) - * @type {number} - * @default 120000 - * @optional - */ - ttl?: number; - - /** - * Subname requesting the ABDC Permission - * @type {string} - */ - subname: string - - /** - * Subname requesting the MApps Permission - * @type {string} - */ - mApp: string -} - -/** - * Represents the response to a request to challenge a specific address using SIWE. - * @interface RequestAppendMAppFieldChallengeResponse - * @public - */ -export interface RequestAppendMAppFieldChallengeResponse extends IResponse{ - /** - * Represents the challenge to be signed by the user. - * @type {string} - */ - challenge: string; -} - -export interface RequestAppendMAppFieldChallengeRoute extends IRoute {} \ No newline at end of file diff --git a/packages/@justaname.id/sdk/src/lib/types/siwe/index.ts b/packages/@justaname.id/sdk/src/lib/types/siwe/index.ts index 065120fe..ca7981d3 100644 --- a/packages/@justaname.id/sdk/src/lib/types/siwe/index.ts +++ b/packages/@justaname.id/sdk/src/lib/types/siwe/index.ts @@ -1,6 +1,3 @@ export * from './request-challenge' export * from './verify-challenge' -export * from './add-mApp-permission-challenge' -export * from './append-mApp-field-challenge' -export * from './revoke-mApp-permission-challenge' export * from './siwe-config' \ No newline at end of file diff --git a/packages/@justaname.id/sdk/src/lib/types/siwe/revoke-mApp-permission-challenge.ts b/packages/@justaname.id/sdk/src/lib/types/siwe/revoke-mApp-permission-challenge.ts deleted file mode 100644 index 8de57423..00000000 --- a/packages/@justaname.id/sdk/src/lib/types/siwe/revoke-mApp-permission-challenge.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { ChainId, EmptyHeaders, IRequest, IResponse, IRoute } from '../common'; - -export interface RequestRevokeMAppPermissionChallengeRequest extends IRequest { - - /** - * Represents the ENS domain - * @type {string} - */ - domain: string; - - /** - * Represents the ethereum address to be challenged. - * @type {string} - */ - address: string; - - /** - * Represents the origin of the request (e.g. the domain of the website). - * @type {string} - */ - origin: string; - - /** - * Represents the chainId of the blockchain to be used. - * @type {1 | 11155111} - */ - chainId: ChainId; - - /** - * Specifies the time-to-live (TTL) for a variable. - * default: 120000 ms, 2 minutes ( 2 * 60 * 1000 ) - * @type {number} - * @default 120000 - * @optional - */ - ttl?: number; - - /** - * Subname requesting the ABDC Permission - * @type {string} - */ - subname: string - - /** - * Subname requesting the MApps Permission - * @type {string} - */ - mApp: string -} - -/** - * Represents the response to a request to challenge a specific address using SIWE. - * @interface RequestRevokeMAppPermissionChallengeResponse - * @public - */ -export interface RequestRevokeMAppPermissionChallengeResponse extends IResponse{ - /** - * Represents the challenge to be signed by the user. - * @type {string} - */ - challenge: string; -} - -export interface RequestRevokeMAppPermissionChallengeRoute extends IRoute {} \ No newline at end of file diff --git a/packages/@justaname.id/sdk/src/lib/utils/ethersCompat.ts b/packages/@justaname.id/sdk/src/lib/utils/ethersCompat.ts deleted file mode 100644 index 682e9f25..00000000 --- a/packages/@justaname.id/sdk/src/lib/utils/ethersCompat.ts +++ /dev/null @@ -1,51 +0,0 @@ -// inspired by spruceid siwe: https://github.com/spruceid/siwe/blob/main/packages/siwe/lib/ethersCompat.ts - -import { ethers } from 'ethers'; - -// @ts-expect-error -- compatibility hack -type ProviderV5 = ethers.providers.Provider; -type ProviderV6 = ethers.Provider; -// @ts-expect-error -- compatibility hack -type JsonRpcProviderV5 = ethers.providers.JsonRpcProvider; -type JsonRpcProviderV6 = ethers.JsonRpcProvider; - -export type Provider = ProviderV6 extends undefined ? ProviderV5 : ProviderV6; -export type JsonRpcProvider = JsonRpcProviderV6 extends undefined - ? JsonRpcProviderV5 - : JsonRpcProviderV6; - -interface EthersCompat { - namehash?: (name: string) => string; - getAddress?: (address: string) => string; - JsonRpcProvider?: new (...args: any[]) => JsonRpcProvider; - utils: { - namehash: (name: string) => string; - getAddress: (address: string) => string; - }; - providers: { - JsonRpcProvider: new (...args: any[]) => JsonRpcProvider; - }; -} - -const ethersCompat = ethers as unknown as EthersCompat; - -export const getJsonRpcProvider = ( - providerUrl?: string, - chainId?: number -): JsonRpcProvider => { - if ('JsonRpcProvider' in ethersCompat) { - return new ethersCompat.JsonRpcProvider!(providerUrl, chainId); - } else { - return new ethersCompat.providers.JsonRpcProvider(providerUrl, chainId); - } -}; - -export const namehash: (name: string) => string = - 'namehash' in ethersCompat - ? ethersCompat.namehash! - : ethersCompat.utils.namehash; - -export const getAddress: (address: string) => string = - 'getAddress' in ethersCompat - ? ethersCompat.getAddress! - : ethersCompat.utils.getAddress; diff --git a/packages/@justaname.id/sdk/src/test/features/sign-in/sign-in.spec.ts b/packages/@justaname.id/sdk/src/test/features/sign-in/sign-in.spec.ts index 7ad6d5a5..2b55366c 100644 --- a/packages/@justaname.id/sdk/src/test/features/sign-in/sign-in.spec.ts +++ b/packages/@justaname.id/sdk/src/test/features/sign-in/sign-in.spec.ts @@ -1,4 +1,8 @@ -import { ethers } from 'ethers'; +import { + generatePrivateKey, + privateKeyToAccount, + type PrivateKeyAccount, +} from 'viem/accounts'; import * as dotenv from 'dotenv'; import SignIn from '../../../lib/features/sign-in'; import { OffchainResolvers } from '../../../lib/features'; @@ -14,18 +18,34 @@ const URI = 'https://' + DOMAIN; const CHAIN_ID = (parseInt(process.env["SDK_CHAIN_ID"] as string) || 11155111) as ChainId const VALID_TTL = 60 * 60 * 24 * 1000; // 1 day -const invalidSigner = new ethers.Wallet( - ethers.Wallet.createRandom().privateKey -); +interface TestSigner { + address: string; + signMessage(message: string): Promise; +} +const toTestSigner = (account: PrivateKeyAccount): TestSigner => ({ + address: account.address, + signMessage: (message: string) => account.signMessage({ message }), +}); +const randomTestSigner = (): TestSigner => + toTestSigner(privateKeyToAccount(generatePrivateKey())); + +const invalidSigner = randomTestSigner(); const ENS_DOMAIN = process.env['SDK_ENS_DOMAIN'] as string; -const subnameSigner = ethers.Wallet.createRandom(); +const subnameSigner = randomTestSigner(); const subnameToBeAdded = Math.random().toString(36).substring(7); const validApiKey = process.env['SDK_JUSTANAME_TEST_API_KEY'] as string; const JUSTANAME_ENV = process.env['SDK_JUSTANAME_DEV'] === 'true'; const SEPOLIA_PROVIDER_URL = process.env['SDK_SEPOLIA_PROVIDER_URL'] as string; const MAINNET_PROVIDER_URL = process.env['SDK_MAINNET_PROVIDER_URL'] as string; -describe('SignIn', () => { +// Integration tests require a live API key + provider URLs. Skip cleanly +// when env is not configured so unit-level assertions can still run. +const INTEGRATION_ENABLED = Boolean( + validApiKey && SEPOLIA_PROVIDER_URL && MAINNET_PROVIDER_URL +); +const describeIntegration = INTEGRATION_ENABLED ? describe : describe.skip; + +describeIntegration('SignIn', () => { let signIn: SignIn; let justaname: JustaName; @@ -112,7 +132,7 @@ describe('SignIn', () => { address: invalidSigner.address, ens: subnameToBeAdded + '.' + ENS_DOMAIN, }); - const signer2 = new ethers.Wallet(ethers.Wallet.createRandom().privateKey); + const signer2 = randomTestSigner(); const signature = await signer2.signMessage(message); try { await signIn.signIn({ message, signature }); diff --git a/packages/@justaname.id/sdk/src/test/features/subname-challenge/subname-challenge.spec.ts b/packages/@justaname.id/sdk/src/test/features/subname-challenge/subname-challenge.spec.ts index 9e48fbaa..9a04b389 100644 --- a/packages/@justaname.id/sdk/src/test/features/subname-challenge/subname-challenge.spec.ts +++ b/packages/@justaname.id/sdk/src/test/features/subname-challenge/subname-challenge.spec.ts @@ -1,10 +1,14 @@ import { SubnameChallenge } from '../../../lib/features/subname-challenge'; // import rest from '../../../lib/api/rest'; import dotenv from 'dotenv'; -import { ethers } from 'ethers'; +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; dotenv.config(); const JUSTANAME_ENV = process.env['SDK_JUSTANAME_DEV'] === 'true' -const signer = ethers.Wallet.createRandom(); +const account = privateKeyToAccount(generatePrivateKey()); +const signer = { + address: account.address, + signMessage: (message: string) => account.signMessage({ message }), +}; describe('subnameChallenge', () => { let subnameChallenge: SubnameChallenge; diff --git a/packages/@justaname.id/sdk/src/test/integration/justaname.spec.ts b/packages/@justaname.id/sdk/src/test/integration/justaname.spec.ts index 6dccf031..eec2f855 100644 --- a/packages/@justaname.id/sdk/src/test/integration/justaname.spec.ts +++ b/packages/@justaname.id/sdk/src/test/integration/justaname.spec.ts @@ -1,26 +1,44 @@ import { JustaName } from '../../lib/justaname'; import { configureEnv } from '../helpers/configureEnv'; import { initializeJustaName } from '../helpers/initializeJustaName'; -import { ethers } from 'ethers'; +import { + generatePrivateKey, + privateKeyToAccount, + type PrivateKeyAccount, +} from 'viem/accounts'; import * as dotenv from 'dotenv'; import { ChainId } from '../../lib/types'; import { ChallengeRequestException } from '../../lib/errors/ChallengeRequest.expection'; dotenv.config(); +interface TestSigner { + address: string; + signMessage(message: string): Promise; +} + +const toTestSigner = (account: PrivateKeyAccount): TestSigner => ({ + address: account.address, + signMessage: (message: string) => account.signMessage({ message }), +}); + const validApiKey = process.env['SDK_JUSTANAME_TEST_API_KEY'] as string; jest.setTimeout(50000); -const mAppPk = process.env['SDK_MAPP_PRIVATE_KEY'] as string; -const mAppSigner = new ethers.Wallet(mAppPk); -const subnameSigner = ethers.Wallet.createRandom(); +const subnameSigner = toTestSigner(privateKeyToAccount(generatePrivateKey())); const subnameToBeAdded = Math.random().toString(36).substring(6); const CHAIN_ID = parseInt(process.env['SDK_CHAIN_ID'] as string) as ChainId; const ENS_DOMAIN = process.env['SDK_ENS_DOMAIN'] as string; -const MAPP = process.env['SDK_MAPP'] as string; -const MAPP_2 = MAPP.split('.')[0] + '2' + '.' + MAPP.split('.')[1]; const SEPOLIA_PROVIDER_URL = process.env['SDK_SEPOLIA_PROVIDER_URL'] as string; const MAINNET_PROVIDER_URL = process.env['SDK_MAINNET_PROVIDER_URL'] as string; -describe('justaname', () => { + +// Integration tests require live env vars (API key, provider URLs, ENS +// domain). Skip the entire suite cleanly when not configured. +const INTEGRATION_ENABLED = Boolean( + validApiKey && ENS_DOMAIN && SEPOLIA_PROVIDER_URL && MAINNET_PROVIDER_URL +); +const describeIntegration = INTEGRATION_ENABLED ? describe : describe.skip; + +describeIntegration('justaname', () => { let justaname: JustaName; beforeAll(async () => { @@ -353,37 +371,6 @@ describe('justaname', () => { }); }); - it("mApps shouldn't be updated", async () => { - const challenge = await justaname.siwe.requestChallenge({ - address: subnameSigner.address, - chainId: CHAIN_ID, - }); - - const signature = await subnameSigner.signMessage(challenge.challenge); - - const response = await justaname.subnames.updateSubname( - { - username: subnameToBeAdded, - chainId: CHAIN_ID, - ensDomain: ENS_DOMAIN, - text: { - mApps: 'shouldntBeUpdated', - [`test_${MAPP}`]: 'shouldBeOverrideWhenMAppPermissionIsAdded', - }, - signature, - }, - { - xMessage: challenge.challenge, - xAddress: subnameSigner.address, - } - ); - - const mApps = response.records.texts.find( - (text) => text.key === 'mApps' - )?.value; - expect(mApps).toBeUndefined(); - }); - it('should remove test if value is empty', async () => { const challenge = await justaname.siwe.requestChallenge({ address: subnameSigner.address, @@ -430,95 +417,6 @@ describe('justaname', () => { expect(response).toBeDefined(); }); - it("should return false if ens can't enable mApps", async () => { - const canEnable = await justaname.mApps.canEnableMApps({ - ens: 'justatest2.eth', - chainId: CHAIN_ID, - }); - - expect(canEnable).toBeFalsy(); - }); - - it('should return true if ens can enable mApps', async () => { - const canEnable = await justaname.mApps.canEnableMApps({ - ens: subnameToBeAdded + '.' + ENS_DOMAIN, - chainId: CHAIN_ID, - }); - - expect(canEnable).toBeTruthy(); - }); - - it("shouldn't have mApps enabled", async () => { - const subname = await justaname.mApps.checkIfMAppIsEnabled({ - ens: subnameToBeAdded + '.' + ENS_DOMAIN, - mApp: MAPP, - chainId: CHAIN_ID, - }); - - expect(subname).toBeFalsy(); - }); - - it('should add mApps permission', async () => { - const challenge = await justaname.mApps.requestAddMAppPermissionChallenge({ - address: subnameSigner.address, - subname: subnameToBeAdded + '.' + ENS_DOMAIN, - mApp: MAPP, - chainId: CHAIN_ID, - }); - - const signature = await subnameSigner.signMessage(challenge.challenge); - const response = await justaname.mApps.addMAppPermission({ - address: subnameSigner.address, - signature, - message: challenge.challenge, - }); - - expect(response).toBeDefined(); - }); - - it('should add mApps2 permission', async () => { - const challenge = await justaname.mApps.requestAddMAppPermissionChallenge({ - address: subnameSigner.address, - subname: subnameToBeAdded + '.' + ENS_DOMAIN, - mApp: MAPP_2, - chainId: CHAIN_ID, - }); - - const signature = await subnameSigner.signMessage(challenge.challenge); - const response = await justaname.mApps.addMAppPermission({ - address: subnameSigner.address, - signature, - message: challenge.challenge, - }); - - expect(response).toBeDefined(); - }); - - it('should have removed test_mApps', async () => { - const subname = await justaname.subnames.getRecords({ - ens: subnameToBeAdded + '.' + ENS_DOMAIN, - chainId: CHAIN_ID, - }); - - const testMApps = subname.records.texts.find( - (text) => text.key === `test_${MAPP}` - )?.value; - - expect(testMApps).toBeUndefined(); - }); - - it('should have mApps enabled', async () => { - const mapp = await justaname.mApps.checkIfMAppIsEnabled({ - ens: subnameToBeAdded + '.' + ENS_DOMAIN, - mApp: MAPP, - chainId: CHAIN_ID, - }); - - expect(mapp).toBeTruthy(); - }); - - - it('should be remove contentHash', async () => { const records = await justaname.subnames.getRecords({ ens: subnameToBeAdded + '.' + ENS_DOMAIN, @@ -561,33 +459,6 @@ describe('justaname', () => { expect(response.records.contentHash).toBeNull(); }); - it('should revoke mApps permission', async () => { - const challenge = - await justaname.mApps.requestRevokeMAppPermissionChallenge({ - subname: subnameToBeAdded + '.' + ENS_DOMAIN, - address: subnameSigner.address, - mApp: MAPP, - chainId: CHAIN_ID, - }); - - const signature = await subnameSigner.signMessage(challenge.challenge); - - const response = await justaname.mApps.revokeMAppPermission({ - address: subnameSigner.address, - signature, - message: challenge.challenge, - }); - - const mApps = response.records.texts.find( - (text) => text.key === 'mApps' - )?.value; - const testJawEth = response.records.texts.find( - (text) => text.key === `test_${MAPP}` - )?.value; - - expect(mApps).toEqual(`{"mApps":["${MAPP_2}"]}`); - expect(testJawEth).toEqual(undefined); - }); it('should get all subnames', async () => { const subnames = await justaname.subnames.getSubnamesByAddress({ diff --git a/packages/@justaname.id/siwens/package.json b/packages/@justaname.id/siwens/package.json index 362acfc1..1125c483 100644 --- a/packages/@justaname.id/siwens/package.json +++ b/packages/@justaname.id/siwens/package.json @@ -1,9 +1,12 @@ { "name": "@justaname.id/siwens", "version": "0.0.145", + "dependencies": { + "punycode": "^2.3.1" + }, "peerDependencies": { - "ethers": "^5.6.8 || ^6.0.8", - "siwe": ">=2.0.0" + "siwe": ">=2.0.0", + "viem": "^2.48.0" }, "exports": { "./package.json": "./dist/package.json", diff --git a/packages/@justaname.id/siwens/src/lib/siwens/siwens.ts b/packages/@justaname.id/siwens/src/lib/siwens/siwens.ts index c85ddcb7..735df385 100644 --- a/packages/@justaname.id/siwens/src/lib/siwens/siwens.ts +++ b/packages/@justaname.id/siwens/src/lib/siwens/siwens.ts @@ -18,7 +18,30 @@ import { extractDataFromStatement, } from '../utils'; import { toASCII, toUnicode } from 'punycode'; -import { getJsonRpcProvider, JsonRpcProvider } from '../utils/ethersCompat'; +import { + createPublicClient, + http, + PublicClient, + isAddressEqual, + getAddress as viemGetAddress, +} from 'viem'; +import { mainnet, sepolia } from 'viem/chains'; +import type { Chain } from 'viem'; +import { normalize } from 'viem/ens'; + +const SUPPORTED_CHAINS: Record = { + 1: mainnet, + 11155111: sepolia, +}; + +const buildPublicClient = ( + providerUrl?: string, + chainId?: number +): PublicClient => + createPublicClient({ + chain: SUPPORTED_CHAINS[chainId ?? 1] ?? mainnet, + transport: http(providerUrl), + }); export interface SiwensResponse extends SiweResponse { ens: string; @@ -40,7 +63,7 @@ export interface SiwensConfig { } export class SIWENS extends SiweMessage { - readonly provider: JsonRpcProvider; + readonly provider: PublicClient; readonly providerUrl: string | undefined; constructor(signInConfig: SiwensConfig) { @@ -50,7 +73,7 @@ export class SIWENS extends SiweMessage { if (!providerUrl) { throw InvalidConfigurationException.providerUrlRequired(); } - this.provider = getJsonRpcProvider(providerUrl); + this.provider = buildPublicClient(providerUrl, this.chainId); this.providerUrl = providerUrl; return; } @@ -91,7 +114,7 @@ export class SIWENS extends SiweMessage { expirationTime, }); this.providerUrl = providerUrl; - this.provider = getJsonRpcProvider(providerUrl); + this.provider = buildPublicClient(providerUrl, this.chainId); } override async verify( @@ -151,12 +174,14 @@ export class SIWENS extends SiweMessage { } private async verifyEnsAddress(ens: string, address: string) { - const resolvedAddress = await this.provider.resolveName(ens); + const resolvedAddress = await this.provider.getEnsAddress({ + name: normalize(ens), + }); if (!resolvedAddress) { throw InvalidENSException.notRegisteredENS(ens); } - if (resolvedAddress !== address) { + if (!isAddressEqual(resolvedAddress, viemGetAddress(address))) { throw InvalidENSException.invalidENSOwner(ens, address); } return true; diff --git a/packages/@justaname.id/siwens/src/lib/utils/ethersCompat.ts b/packages/@justaname.id/siwens/src/lib/utils/ethersCompat.ts deleted file mode 100644 index 682e9f25..00000000 --- a/packages/@justaname.id/siwens/src/lib/utils/ethersCompat.ts +++ /dev/null @@ -1,51 +0,0 @@ -// inspired by spruceid siwe: https://github.com/spruceid/siwe/blob/main/packages/siwe/lib/ethersCompat.ts - -import { ethers } from 'ethers'; - -// @ts-expect-error -- compatibility hack -type ProviderV5 = ethers.providers.Provider; -type ProviderV6 = ethers.Provider; -// @ts-expect-error -- compatibility hack -type JsonRpcProviderV5 = ethers.providers.JsonRpcProvider; -type JsonRpcProviderV6 = ethers.JsonRpcProvider; - -export type Provider = ProviderV6 extends undefined ? ProviderV5 : ProviderV6; -export type JsonRpcProvider = JsonRpcProviderV6 extends undefined - ? JsonRpcProviderV5 - : JsonRpcProviderV6; - -interface EthersCompat { - namehash?: (name: string) => string; - getAddress?: (address: string) => string; - JsonRpcProvider?: new (...args: any[]) => JsonRpcProvider; - utils: { - namehash: (name: string) => string; - getAddress: (address: string) => string; - }; - providers: { - JsonRpcProvider: new (...args: any[]) => JsonRpcProvider; - }; -} - -const ethersCompat = ethers as unknown as EthersCompat; - -export const getJsonRpcProvider = ( - providerUrl?: string, - chainId?: number -): JsonRpcProvider => { - if ('JsonRpcProvider' in ethersCompat) { - return new ethersCompat.JsonRpcProvider!(providerUrl, chainId); - } else { - return new ethersCompat.providers.JsonRpcProvider(providerUrl, chainId); - } -}; - -export const namehash: (name: string) => string = - 'namehash' in ethersCompat - ? ethersCompat.namehash! - : ethersCompat.utils.namehash; - -export const getAddress: (address: string) => string = - 'getAddress' in ethersCompat - ? ethersCompat.getAddress! - : ethersCompat.utils.getAddress; diff --git a/packages/@justaname.id/siwens/src/test/siwens.spec.ts b/packages/@justaname.id/siwens/src/test/siwens.spec.ts index 8b1335ca..5a5c9ae7 100644 --- a/packages/@justaname.id/siwens/src/test/siwens.spec.ts +++ b/packages/@justaname.id/siwens/src/test/siwens.spec.ts @@ -1,4 +1,8 @@ -import { ethers } from 'ethers'; +import { + generatePrivateKey, + privateKeyToAccount, + type PrivateKeyAccount, +} from 'viem/accounts'; import * as dotenv from 'dotenv'; import { SIWENS, @@ -7,8 +11,31 @@ import { } from '../'; dotenv.config(); -const pk = process.env['SIWENS_PRIVATE_KEY'] as string; -const signer = new ethers.Wallet(pk); +interface TestSigner { + address: string; + signMessage(message: string): Promise; +} +const toTestSigner = (account: PrivateKeyAccount): TestSigner => ({ + address: account.address, + signMessage: (message: string) => account.signMessage({ message }), +}); +const randomTestSigner = (): TestSigner => + toTestSigner(privateKeyToAccount(generatePrivateKey())); + +// Integration tests are gated on env vars — they hit a real Sepolia provider +// and require a funded test wallet. When the env is not configured we still +// want unit-level tests (TTL validation, ENS format, nonce) to run, so we +// stub `signer` with a deterministic random account and use `itIntegration` +// for tests that actually need the configured wallet/provider. +const rawPk = process.env['SIWENS_PRIVATE_KEY']; +// CI sets unconfigured env vars to the empty string (not undefined), so we +// need to treat empty as "not provided" before handing it to viem. +const pk = rawPk && rawPk.startsWith('0x') ? (rawPk as `0x${string}`) : undefined; +const INTEGRATION_ENABLED = Boolean(pk && process.env['SIWENS_PROVIDER_URL']); +const itIntegration = INTEGRATION_ENABLED ? it : it.skip; +const signer = toTestSigner( + privateKeyToAccount(pk ?? generatePrivateKey()) +); const PROVIDER_URL = process.env['SIWENS_PROVIDER_URL'] as string; const DOMAIN = 'justaname.id'; const URI = 'https://' + DOMAIN; @@ -20,7 +47,9 @@ const VALID_TTL = 60 * 60 * 24 * 1000; // 1 day const TTL_LESS_THAN_ZERO = -1; const TTL_GREATER_THAN_MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER + 1; const INVALID_ENS = 'justaname'; -const VALID_ENS = process.env['SIWENS_VALID_ENS'] as string; +// Fallback ENS for unit-level tests (the value is only consequential for +// the integration tests guarded by `itIntegration`). +const VALID_ENS = (process.env['SIWENS_VALID_ENS'] as string) || 'test.eth'; describe('SIWENS', () => { @@ -132,7 +161,7 @@ describe('SIWENS', () => { expect(signature).toBeTruthy(); }); - it('should verify a valid signature', async () => { + itIntegration('should verify a valid signature', async () => { const signature = await signer.signMessage(message); const address = await new SIWENS({ @@ -144,7 +173,7 @@ describe('SIWENS', () => { expect(address.success).toBeTruthy(); },60000) - it('should return ens in the response', async () => { + itIntegration('should return ens in the response', async () => { const signature = await signer.signMessage(message); const address = await new SIWENS({ params:message, @@ -155,8 +184,8 @@ describe('SIWENS', () => { expect(address.ens).toBe(VALID_ENS); },60000) - it('should return ens in the failed response', async () => { - const signer2 = ethers.Wallet.createRandom(); + itIntegration('should return ens in the failed response', async () => { + const signer2 = randomTestSigner(); const signature = await signer2.signMessage(message); try { await new SIWENS({ @@ -172,8 +201,8 @@ describe('SIWENS', () => { throw new Error('Should have thrown an error'); },60000) - it('should throw an error if address isn\'t owner of ens', async () => { - const signer = ethers.Wallet.createRandom(); + itIntegration('should throw an error if address isn\'t owner of ens', async () => { + const signer = randomTestSigner(); const siwens = new SIWENS({ params: { domain: DOMAIN, diff --git a/packages/@justverified/plugin/src/lib/components/EmailCredentialItem/index.tsx b/packages/@justverified/plugin/src/lib/components/EmailCredentialItem/index.tsx index e9273288..9f4b9bfa 100644 --- a/packages/@justverified/plugin/src/lib/components/EmailCredentialItem/index.tsx +++ b/packages/@justverified/plugin/src/lib/components/EmailCredentialItem/index.tsx @@ -11,7 +11,6 @@ export interface EmailCredentialItemProps { credentialValue: EthereumEip712Signature2021<{ email: string }> | undefined; disabled?: boolean; refetchRecords: () => void; - mAppsAlreadyEnabled: string[] | undefined; mApp: string; } @@ -22,7 +21,6 @@ export const EmailCredentialItem: FC = ({ verificationBackendUrl, disabled = false, refetchRecords, - mAppsAlreadyEnabled, mApp, }) => { const [email, setEmail] = useState(''); @@ -65,7 +63,6 @@ export const EmailCredentialItem: FC = ({ verificationBackendUrl={verificationBackendUrl} refetchVerifyRecords={refetchVerifyRecords} refetchRecords={refetchRecords} - mAppsAlreadyEnabled={mAppsAlreadyEnabled} mApp={mApp} /> void; refetchRecords: () => void; - mAppsAlreadyEnabled: string[] | undefined; mApp: string; open: boolean; email: string | undefined; @@ -38,7 +37,6 @@ export interface EmailDialogProps { export const EmailDialog: FC = ({ refetchRecords, mApp, - mAppsAlreadyEnabled, refetchVerifyRecords, open, email, @@ -250,35 +248,21 @@ export const EmailDialog: FC = ({ const key = 'email'; const vc = res.verifiableCredential; const value = vc.credentialSubject.email; - if (mAppsAlreadyEnabled?.includes(mApp)) { - updateRecords({ - text: [ - { - key: key, - value: value, - }, - ], - }).then(() => { - refetchRecords(); - refetchVerifyRecords(); - }); - } else { - updateRecords({ - text: [ - { - key: key, - value: value, - }, - { - key: res.dataKey, - value: JSON.stringify(vc), - }, - ], - }).then(() => { - refetchRecords(); - refetchVerifyRecords(); - }); - } + updateRecords({ + text: [ + { + key: key, + value: value, + }, + { + key: res.dataKey, + value: JSON.stringify(vc), + }, + ], + }).then(() => { + refetchRecords(); + refetchVerifyRecords(); + }); handleInternalOpenDialog(false); }); }} diff --git a/packages/@justverified/plugin/src/lib/dialogs/JustVerifiedDialog/index.tsx b/packages/@justverified/plugin/src/lib/dialogs/JustVerifiedDialog/index.tsx index 9ee70eb8..fddcd0d9 100644 --- a/packages/@justverified/plugin/src/lib/dialogs/JustVerifiedDialog/index.tsx +++ b/packages/@justverified/plugin/src/lib/dialogs/JustVerifiedDialog/index.tsx @@ -5,7 +5,6 @@ import { JustaNameDialog, JustWeb3Context, useJustWeb3, - useMApps, } from '@justweb3/widget'; import { FC, Fragment, useContext, useEffect, useState } from 'react'; import { EmailCredentialItem } from '../../components/EmailCredentialItem'; @@ -47,7 +46,6 @@ export const JustVerifiedDialog: FC = ({ selectedCredential, ]); const { connectedEns, updateRecords } = useJustWeb3(); - const { mAppsAlreadyEnabled } = useMApps(); const { refetchRecords } = useRecords({ ens: connectedEns?.ens || '', }); @@ -203,35 +201,21 @@ export const JustVerifiedDialog: FC = ({ socialValue = ''; } } - if (mAppsAlreadyEnabled?.includes(mApp)) { - updateRecords({ - text: [ - { - key: socialKey, - value: socialValue, - }, - ], - }).then(() => { - refetchRecords(); - refetchVerifyRecords(); - }); - } else { - updateRecords({ - text: [ - { - key: socialKey, - value: socialValue, - }, - { - key: credentialKey, - value: JSON.stringify(credentialValue), - }, - ], - }).then(() => { - refetchRecords(); - refetchVerifyRecords(); - }); - } + updateRecords({ + text: [ + { + key: socialKey, + value: socialValue, + }, + { + key: credentialKey, + value: JSON.stringify(credentialValue), + }, + ], + }).then(() => { + refetchRecords(); + refetchVerifyRecords(); + }); }) .catch((error) => { setSelectedCredential(undefined); @@ -263,7 +247,6 @@ export const JustVerifiedDialog: FC = ({ ) : ( { pluginApi.setState('verificationOpen', false); }, - onMAppAdd: (pluginApi, ens, mApp) => { - pluginApi.setState('verificationOpen', true); - }, }, }); diff --git a/packages/@justweb3/ui/src/lib/icons/components/general/Mapp.tsx b/packages/@justweb3/ui/src/lib/icons/components/general/Mapp.tsx deleted file mode 100644 index 9797920a..00000000 --- a/packages/@justweb3/ui/src/lib/icons/components/general/Mapp.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { SVGProps } from 'react'; -export default function Mapp(props: SVGProps) { - return ( - - - - - - - - - ); -} diff --git a/packages/@justweb3/ui/src/lib/icons/components/general/index.ts b/packages/@justweb3/ui/src/lib/icons/components/general/index.ts index d78fdac0..bd8539f2 100644 --- a/packages/@justweb3/ui/src/lib/icons/components/general/index.ts +++ b/packages/@justweb3/ui/src/lib/icons/components/general/index.ts @@ -29,7 +29,6 @@ import GeneralIcon from './General'; import LocationOnIcon from './LocationOn'; import LocationIcon from './Location'; import LogoutIcon from './Logout'; -import MappIcon from './Mapp'; import MaximizeIcon from './Maximize'; import MicIcon from './Mic'; import MinimizeIcon from './Minimize'; @@ -87,7 +86,6 @@ const general = { 'location-on': LocationOnIcon, location: LocationIcon, logout: LogoutIcon, - mapp: MappIcon, maximize: MaximizeIcon, mic: MicIcon, minimize: MinimizeIcon, @@ -148,7 +146,6 @@ export { LocationOnIcon, LocationIcon, LogoutIcon, - MappIcon, MaximizeIcon, MicIcon, MinimizeIcon, diff --git a/packages/@justweb3/widget/src/lib/components/JustWeb3Button/index.tsx b/packages/@justweb3/widget/src/lib/components/JustWeb3Button/index.tsx index 6d716993..a23eaf42 100644 --- a/packages/@justweb3/widget/src/lib/components/JustWeb3Button/index.tsx +++ b/packages/@justweb3/widget/src/lib/components/JustWeb3Button/index.tsx @@ -2,8 +2,6 @@ import { Records, useAccountEnsNames, useAccountSubnames, - useCanEnableMApps, - useEnabledMApps, useEnsAvatar, useMountedAccount, useOffchainResolvers, @@ -21,20 +19,17 @@ import { formatText, LoadingSpinner, LogoutIcon, - MappIcon, P, Popover, PopoverTrigger, ProfileIcon, SettingsIcon, SwitchAccountIcon, - SPAN, } from '@justweb3/ui'; import { FC, ReactNode, useContext, useMemo, useState } from 'react'; import { useDisconnect } from 'wagmi'; import { ConfigurationDialog, PrimaryNamesDialog } from '../../dialogs'; import { DefaultDialog } from '../../dialogs/DefaultDialog'; -import { MAppsDialog } from '../../dialogs/MAppsDialog'; import { getChainIcon } from '../../icons/chain-icons'; import { getTextRecordIcon } from '../../icons/records-icons'; import { JustWeb3Context, useJustWeb3 } from '../../providers'; @@ -57,11 +52,10 @@ export const JustWeb3Button: FC = ({ logout, style, }) => { - const [openMApps, setOpenMApps] = useState(false); const [openSettings, setOpenSettings] = useState(false); const [openPrimaryNames, setOpenPrimaryNames] = useState(false); const [openConfiguration, setOpenConfiguration] = useState(false); - const { plugins, mApps, config } = useContext(JustWeb3Context); + const { plugins, config } = useContext(JustWeb3Context); const { createPluginApi } = useContext(PluginContext); const { address, isConnected, chainId } = useMountedAccount(); const [mobileDialogOpen, setMobileDialogOpen] = useState(false); @@ -74,22 +68,10 @@ export const JustWeb3Button: FC = ({ handleOpenSignInDialog, openEnsProfile, } = useJustWeb3(); - const { canEnableMApps, isCanEnableMAppsPending } = useCanEnableMApps({ - ens: connectedEns?.ens || '', - }); const { offchainResolvers } = useOffchainResolvers(); - const { enabledMApps } = useEnabledMApps({ - ens: connectedEns?.ens || '', - }); const { records, isRecordsPending } = useRecords({ ens: connectedEns?.ens }); - const mAppsToEnable = useMemo(() => { - if (!mApps || !enabledMApps) { - return undefined; - } - return mApps.filter((mApp) => !enabledMApps.includes(mApp)); - }, [mApps, enabledMApps]); const { avatar } = useEnsAvatar({ ens: connectedEns?.ens, @@ -129,12 +111,6 @@ export const JustWeb3Button: FC = ({ ); }, [records]); - const handleOpenMAppsDialog = (open: boolean) => { - if (open !== openMApps) { - setOpenMApps(open); - } - }; - const handleOpenPrimaryNamesDialog = (open: boolean) => { if (open !== openPrimaryNames) { setOpenPrimaryNames(open); @@ -482,37 +458,6 @@ export const JustWeb3Button: FC = ({ ); })} - } - title={'mApps'} - style={{ - width: '100%', - display: 'none', - }} - onClick={() => setOpenMApps(true)} - right={ - - {mAppsToEnable && canEnableMApps && mAppsToEnable.length > 0 && ( - - Configuration Required - - )} - - - } - disabled={!canEnableMApps} - loading={isCanEnableMAppsPending} - /> - = ({ return ( <> -
diff --git a/packages/@justweb3/widget/src/lib/dialogs/AuthorizeMAppDialog/index.tsx b/packages/@justweb3/widget/src/lib/dialogs/AuthorizeMAppDialog/index.tsx deleted file mode 100644 index ad14cf91..00000000 --- a/packages/@justweb3/widget/src/lib/dialogs/AuthorizeMAppDialog/index.tsx +++ /dev/null @@ -1,287 +0,0 @@ -import { FC, Fragment, useEffect, useMemo, useState } from 'react'; -import { - useAddMAppPermission, - useCanEnableMApps, - useIsMAppEnabled, - useRecords, -} from '@justaname.id/react'; -import { - Badge, - Button, - ClickableItem, - Flex, - H2, - JustaNameLogoIcon, - P, - SPAN, -} from '@justweb3/ui'; -import { isParseable } from '../../utils'; -import { DefaultDialog } from '../DefaultDialog'; - -export interface AuthorizeMAppDialogProps { - mApp: { - name: string; - isOpen: boolean; - }; - handleOpenDialog: (open: boolean) => void; - logo?: string; - isLoggedIn: boolean; - handleOpenSignInDialog: (open: boolean) => void; - connectedEns: string | undefined; - isEnsAuthPending: boolean; - disableOverlay?: boolean; -} - -export const AuthorizeMAppDialog: FC = ({ - mApp: { name: mApp, isOpen: open }, - handleOpenDialog, - logo, - isLoggedIn, - handleOpenSignInDialog, - connectedEns, - isEnsAuthPending, - disableOverlay, -}) => { - const [openOnConnect] = useState(open); - const { records: mAppRecords, isRecordsPending: isMAppRecordsPending } = - useRecords({ - ens: mApp || '', - }); - const { records } = useRecords({ - ens: connectedEns || '', - }); - const { canEnableMApps, isCanEnableMAppsPending } = useCanEnableMApps({ - ens: connectedEns || '', - }); - const { isMAppEnabled, isMAppEnabledPending } = useIsMAppEnabled({ - ens: connectedEns || '', - mApp, - }); - const { addMAppPermission, isAddMAppPermissionPending } = - useAddMAppPermission({ - mApp, - }); - - const mAppFieldsInEnsRecords = useMemo(() => { - return records?.records.texts?.filter((text) => - text.key.endsWith(`_${mApp}`) - ); - }, [records, mApp]); - - const mAppDescription = useMemo(() => { - return mAppRecords?.records.texts?.find( - (text) => text.key === `mApp_description` - )?.value; - }, [mAppRecords]); - - const mAppPermissions = useMemo((): string[] => { - if (!mAppRecords) { - return []; - } - - const permissions = mAppRecords?.records.texts?.find( - (text) => text.key === `mApp_permissions` - )?.value; - if (!permissions) { - return []; - } - - if (!isParseable(permissions)) { - return []; - } - - const parsedPermissions = JSON.parse(permissions) as string[]; - if (!Array.isArray(parsedPermissions)) { - return []; - } - - return parsedPermissions; - }, [mAppRecords]); - - const handleOpenDialogInternal = (_open: boolean) => { - if (!connectedEns) { - handleOpenSignInDialog(true); - return; - } - - if (_open !== open) { - handleOpenDialog(_open); - } - }; - - useEffect(() => { - if (connectedEns) { - if (isCanEnableMAppsPending) { - return; - } - - if (!canEnableMApps && canEnableMApps !== undefined) { - handleOpenDialogInternal(false); - return; - } - if (isLoggedIn) { - if (isMAppEnabledPending || isMAppEnabled === undefined) { - return; - } - handleOpenDialogInternal(!isMAppEnabled && openOnConnect); - } else { - handleOpenDialogInternal(false); - } - } - }, [ - isMAppEnabled, - isLoggedIn, - isMAppEnabledPending, - connectedEns, - isCanEnableMAppsPending, - canEnableMApps, - ]); - - if (isEnsAuthPending || !connectedEns || !records) { - return null; - } - - if ( - (isMAppRecordsPending || isCanEnableMAppsPending || isMAppEnabledPending) && - open - ) { - return null; - } - - return ( - handleOpenDialogInternal(false)} - header={ -
- {logo ? ( - logo - ) : ( - - )} -
- } - disableOverlay={disableOverlay} - > - - - - - {connectedEns} - - - -

Authorise {mApp} mApp

-
-
- - - {!mAppDescription && !mAppPermissions.length && ( -

- This mApp does not have any permissions or description, be - cautious when authorising it. -

- )} - - {mAppDescription && ( - -

{mAppDescription}

-
- )} - - {mAppPermissions?.length > 0 || - (mAppFieldsInEnsRecords && mAppFieldsInEnsRecords?.length > 0) ? ( - - {mAppPermissions.map((permission, index) => { - return ( - - -

Permission

- -

{permission}

-
-
- ); - })} - - {mAppFieldsInEnsRecords && mAppFieldsInEnsRecords.length > 0 && ( - <> -

Installing this mApp will remove the following fields:

- - {mAppFieldsInEnsRecords.map((field) => { - return ( - - - - ); - })} - - - )} -
- ) : null} -
- -
-
- ); -}; diff --git a/packages/@justweb3/widget/src/lib/dialogs/MAppsDialog/index.tsx b/packages/@justweb3/widget/src/lib/dialogs/MAppsDialog/index.tsx deleted file mode 100644 index ebbc8b34..00000000 --- a/packages/@justweb3/widget/src/lib/dialogs/MAppsDialog/index.tsx +++ /dev/null @@ -1,268 +0,0 @@ -import { FC, Fragment, useContext, useEffect, useMemo, useState } from 'react'; -import { useEnabledMApps, useEnsAvatar, useRecords } from '@justaname.id/react'; -import { - ArrowIcon, - Avatar, - Badge, - Button, - ClickableItem, - Divider, - Flex, - H2, - JustaNameLogoIcon, - LoadingSpinner, - P, - SPAN, - TrashIcon, -} from '@justweb3/ui'; -import { DefaultDialog } from '../DefaultDialog'; -import { JustWeb3Context, useJustWeb3 } from '../../providers'; -import { useMApps } from '../../providers/MAppProvider'; - -export interface AuthorizeMAppDialogProps { - open: boolean; - handleOpenDialog: (open: boolean) => void; -} - -export const MAppsDialog: FC = ({ - open, - handleOpenDialog, -}) => { - const { - config: { logo, disableOverlay }, - mApps, - } = useContext(JustWeb3Context); - const { handleOpenRevokeMAppDialog, handleOpenAuthorizeMAppDialog } = - useMApps(); - const { connectedEns, isEnsAuthPending } = useJustWeb3(); - const { getRecords } = useRecords(); - const { enabledMApps, isMAppEnabledPending } = useEnabledMApps({ - ens: connectedEns?.ens || '', - }); - const { getEnsAvatar } = useEnsAvatar(); - const [mAppsDescription, setMAppsDescription] = useState< - { mApp: string; description: string }[] | undefined - >(undefined); - const [mAppsAvatar, setMAppsAvatar] = useState< - { mApp: string; avatar: string }[] | undefined - >(undefined); - const mAppsToEnable = useMemo(() => { - if (!mApps || !enabledMApps) { - return undefined; - } - return mApps.filter((mApp) => !enabledMApps.includes(mApp)); - }, [mApps, enabledMApps]); - - const mAppsAlreadyEnabled = useMemo(() => { - if (!mApps || !enabledMApps) { - return undefined; - } - return mApps.filter((mApp) => enabledMApps.includes(mApp)); - }, [mApps, enabledMApps]); - - useEffect(() => { - if (!mApps) { - return; - } - Promise.allSettled(mApps.map((mApp) => getRecords({ ens: mApp }))).then( - (records) => { - setMAppsDescription( - records - ?.filter((record) => record.status === 'fulfilled') - .map((record) => record.value) - .map((record, index) => ({ - mApp: mApps[index], - description: - record?.records?.texts.find( - (text) => text.key === `mApp_description` - )?.value || '', - })) - ); - } - ); - - Promise.allSettled(mApps.map((mApp) => getEnsAvatar({ name: mApp }))).then( - (avatars) => { - setMAppsAvatar( - avatars - ?.filter((avatar) => avatar.status === 'fulfilled') - .map((avatar) => avatar.value) - .map((avatar, index) => ({ - mApp: mApps[index], - avatar: avatar || '', - })) - ); - } - ); - }, [mApps]); - - // if (!connectedEns || isEnsAuthPending || isMAppEnabledPending) { - // return ; - // } - - return ( - handleOpenDialog(false)} - header={ -
- {logo ? ( - logo - ) : ( - - )} -
- } - > - {!connectedEns || isEnsAuthPending || isMAppEnabledPending ? ( -
- -
- ) : ( - - - - {connectedEns.ens} - - - -

mApps Configuration

-
- - - {mAppsToEnable && mAppsToEnable.length > 0 && ( - -

Installable mApps

- - - {mAppsToEnable.map((mApp, index) => { - return ( - - avatar.mApp === mApp - )?.avatar - } - size={34} - /> - } - style={{ - width: '100%', - }} - subtitle={ - mAppsDescription?.find( - (description) => description.mApp === mApp - )?.description - } - clickable={false} - right={ - - } - /> - - ); - })} - -
- )} - - {mAppsToEnable && - mAppsToEnable.length > 0 && - mAppsAlreadyEnabled && - mAppsAlreadyEnabled.length > 0 ? ( - - ) : null} - - {mAppsAlreadyEnabled && mAppsAlreadyEnabled.length > 0 && ( - -

Configured mApps

- - - {mAppsAlreadyEnabled.map((mApp, index) => { - return ( - - avatar.mApp === mApp - )?.avatar - } - size={34} - /> - } - subtitle={ - mAppsDescription?.find( - (description) => description.mApp === mApp - )?.description - } - clickable={false} - style={{ - width: '100%', - }} - right={ - - } - /> - - ); - })} - -
- )} -
-
- )} -
- ); -}; diff --git a/packages/@justweb3/widget/src/lib/dialogs/RevokeMAppDialog/index.tsx b/packages/@justweb3/widget/src/lib/dialogs/RevokeMAppDialog/index.tsx deleted file mode 100644 index 5d3e83db..00000000 --- a/packages/@justweb3/widget/src/lib/dialogs/RevokeMAppDialog/index.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import { FC, Fragment, useEffect, useMemo, useState } from 'react'; -import { - useCanEnableMApps, - useIsMAppEnabled, - useRecords, - useRevokeMAppPermission, -} from '@justaname.id/react'; -import { - Badge, - Button, - ClickableItem, - Flex, - H2, - JustaNameLogoIcon, - P, - SPAN, - TrashWhiteIcon, -} from '@justweb3/ui'; -import { DefaultDialog } from '../DefaultDialog'; - -export interface RevokeMAppDialogProps { - mApp: { - name: string; - isOpen: boolean; - }; - handleOpenDialog: (open: boolean) => void; - logo?: string; - isLoggedIn: boolean; - handleOpenSignInDialog: (open: boolean) => void; - connectedEns: string | undefined; - isEnsAuthPending: boolean; - disableOverlay?: boolean; -} - -export const RevokeMAppDialog: FC = ({ - mApp: { name: mApp, isOpen: open }, - handleOpenDialog, - logo, - isLoggedIn, - handleOpenSignInDialog, - connectedEns, - isEnsAuthPending, - disableOverlay, -}) => { - const [openOnConnect] = useState(open); - const { records: mAppRecords, isRecordsPending: isMAppRecordsPending } = - useRecords({ - ens: mApp || '', - }); - const { records } = useRecords({ - ens: connectedEns || '', - }); - const { canEnableMApps, isCanEnableMAppsPending } = useCanEnableMApps({ - ens: connectedEns || '', - }); - const { isMAppEnabled, isMAppEnabledPending } = useIsMAppEnabled({ - ens: connectedEns || '', - mApp, - }); - const { revokeMAppPermission, isRevokeMAppPermissionPending } = - useRevokeMAppPermission({ - mApp, - }); - - const mAppDescription = useMemo(() => { - return mAppRecords?.records.texts?.find( - (text) => text.key === `mApp_description` - )?.value; - }, [mAppRecords]); - - const mAppFieldsInEnsRecords = useMemo(() => { - return records?.records.texts?.filter((text) => - text.key.endsWith(`_${mApp}`) - ); - }, [records, mApp]); - - const handleOpenDialogInternal = (_open: boolean) => { - if (!connectedEns) { - handleOpenSignInDialog(true); - return; - } - - if (_open !== open) { - handleOpenDialog(_open); - } - }; - - useEffect(() => { - if (connectedEns) { - if (isCanEnableMAppsPending) { - return; - } - - if (!canEnableMApps && canEnableMApps !== undefined) { - handleOpenDialogInternal(false); - return; - } - if (isLoggedIn) { - if (isMAppEnabledPending || isMAppEnabled === undefined) { - return; - } - handleOpenDialogInternal(!isMAppEnabled && openOnConnect); - } else { - handleOpenDialogInternal(false); - } - } - }, [ - isMAppEnabled, - isLoggedIn, - isMAppEnabledPending, - connectedEns, - isCanEnableMAppsPending, - canEnableMApps, - ]); - - if (isEnsAuthPending || !connectedEns || !records) { - return null; - } - - if ( - (isMAppRecordsPending || isCanEnableMAppsPending || isMAppEnabledPending) && - open - ) { - return null; - } - - return ( - handleOpenDialogInternal(false)} - header={ -
- {logo ? ( - logo - ) : ( - - )} -
- } - disableOverlay={disableOverlay} - > - - - - - {connectedEns} - - - - -

Revoke {mApp} mApp

-
-
- - -

{mAppDescription}

-
- - -

Removing this mApp will revoke all permissions granted to it.

- {mAppFieldsInEnsRecords && mAppFieldsInEnsRecords.length > 0 && ( - <> -

The following fields will be removed:

- - {mAppFieldsInEnsRecords.map((field) => { - return ( - - - - ); - })} - - - )} -
- - - - - - -
-
- ); -}; diff --git a/packages/@justweb3/widget/src/lib/dialogs/index.ts b/packages/@justweb3/widget/src/lib/dialogs/index.ts index 34a8d186..d79a6e96 100644 --- a/packages/@justweb3/widget/src/lib/dialogs/index.ts +++ b/packages/@justweb3/widget/src/lib/dialogs/index.ts @@ -1,4 +1,3 @@ -export * from './AuthorizeMAppDialog'; export * from './AvatarSelectorDialog'; export * from './BannerSelectorDialog'; export * from './ConfigurationDialog'; diff --git a/packages/@justweb3/widget/src/lib/plugins/index.ts b/packages/@justweb3/widget/src/lib/plugins/index.ts index 5ba874b9..25e3ec05 100644 --- a/packages/@justweb3/widget/src/lib/plugins/index.ts +++ b/packages/@justweb3/widget/src/lib/plugins/index.ts @@ -10,15 +10,12 @@ export interface PluginApi { isLoggedIn: boolean; chainId: number | undefined; records: UseRecordsResult['records']; - mApps: string[]; setState: (key: string, value: T) => void; getState: (key: string) => T | undefined; config: JustWeb3ProviderConfig; eventEmitter: EventEmitter; - handleOpenAuthorizeMAppDialog: (mApp: string, open: boolean) => void; - handleOpenRevokeMAppDialog: (mApp: string, open: boolean) => void; handleOpenSignInDialog: (open: boolean) => void; } @@ -74,30 +71,18 @@ type OnEnsSignInHook = ( pluginApi: PluginApi, ens: string, chainId: number, - records: UseRecordsResult['records'], - enabledMApps: string[], - canEnableMApps: boolean + records: UseRecordsResult['records'] ) => void; type OnEnsChangeHook = ( pluginApi: PluginApi, ens: string, - records: UseRecordsResult['records'], - enabledMApps: string[], - canEnableMApps: boolean + records: UseRecordsResult['records'] ) => void; type OnEnsSignOutHook = (pluginApi: PluginApi, ens: string) => void; -type OnMAppAddHook = (pluginApi: PluginApi, ens: string, mApp: string) => void; -type OnMAppRemoveHook = ( - pluginApi: PluginApi, - ens: string, - mApp: string -) => void; type OnRecordsChangeHook = ( pluginApi: PluginApi, ens: string, - records: UseRecordsResult['records'], - enabledMApps: string[], - canEnableMApps: boolean + records: UseRecordsResult['records'] ) => void; type OnSubnameClaimedHook = (pluginApi: PluginApi, subname: string) => void; type OnSwitchChain = ( @@ -117,8 +102,6 @@ interface Hooks { onSwitchChain?: OnSwitchChain; onSubnameClaimed?: OnSubnameClaimedHook; onEnsChange?: OnEnsChangeHook; - onMAppAdd?: OnMAppAddHook; - onMAppRemove?: OnMAppRemoveHook; onRecordsChange?: OnRecordsChangeHook; onStateChange?: ( pluginApi: PluginApi, @@ -133,8 +116,6 @@ export interface JustaPlugin { components?: PluginComponents; - mApps?: string[]; - hooks?: Hooks; priority?: number; diff --git a/packages/@justweb3/widget/src/lib/providers/JustWeb3Provider/index.tsx b/packages/@justweb3/widget/src/lib/providers/JustWeb3Provider/index.tsx index 85c80f8c..5a955dd4 100644 --- a/packages/@justweb3/widget/src/lib/providers/JustWeb3Provider/index.tsx +++ b/packages/@justweb3/widget/src/lib/providers/JustWeb3Provider/index.tsx @@ -25,7 +25,7 @@ import { } from '@justaname.id/react'; import { JustWeb3ThemeProvider } from '@justweb3/ui'; import { SignInDialog } from '../../dialogs/SignInDialog'; -import { MAppsProvider } from '../MAppProvider'; +import { PluginProvider } from '../PluginProvider'; import { JustaPlugin } from '../../plugins'; import usePreviousState from '../../hooks/usePreviousState'; import { ProfileDialog, UpdateRecordDialog } from '../../dialogs'; @@ -51,7 +51,6 @@ export interface JustWeb3ContextProps { isSignInOpen: boolean; config: JustWeb3ProviderConfig; plugins: JustaPlugin[]; - mApps: string[]; } export const JustWeb3Context = createContext({ @@ -63,7 +62,6 @@ export const JustWeb3Context = createContext({ handleJustWeb3Config: () => { }, config: {}, plugins: [], - mApps: [], }); export const JustWeb3Provider: FC = ({ @@ -104,20 +102,6 @@ export const JustWeb3Provider: FC = ({ [config.plugins] ); - const pluginsMApps = - (useMemo( - () => - plugins - ?.filter((plugin) => plugin.mApps) - ?.map((plugin) => plugin.mApps) - .flat() - .map((mApp) => ({ - name: mApp, - openOnConnect: false, - })), - [plugins] - ) as { name: string; openOnConnect: boolean }[]) || []; - const handleUpdateRecords = async ( records: UpdateRecordsParams & { ens: string } ) => { @@ -142,27 +126,6 @@ export const JustWeb3Provider: FC = ({ } }, [updateRecord]); - const mAppsWithOpenOnConnect = - config?.mApps?.map((mApp) => { - if (typeof mApp === 'string') { - return { - name: mApp, - openOnConnect: false, - }; - } - return mApp; - }) || []; - - const allMApps = [...mAppsWithOpenOnConnect, ...pluginsMApps].reduce( - (acc, mApp) => { - if (!acc.find((accMApp) => accMApp.name === mApp.name)) { - return [...acc, mApp]; - } - return acc; - }, - [] as { name: string; openOnConnect: boolean }[] - ); - const handleOpenSignInDialog = (open: boolean) => { if (!isConnected) { return; @@ -214,16 +177,13 @@ export const JustWeb3Provider: FC = ({ isSignInOpen: signInOpen, config: config, plugins, - mApps: allMApps.map((mApp) => mApp.name), handleUpdateRecords: handleUpdateRecords, handleJustWeb3Config, handleOpenEnsProfile, handleCloseEnsProfile, }} > - = ({ logo={config.logo} /> {children} - + diff --git a/packages/@justweb3/widget/src/lib/providers/MAppProvider/index.tsx b/packages/@justweb3/widget/src/lib/providers/MAppProvider/index.tsx deleted file mode 100644 index 56cac1bb..00000000 --- a/packages/@justweb3/widget/src/lib/providers/MAppProvider/index.tsx +++ /dev/null @@ -1,322 +0,0 @@ -import { - createContext, - FC, - Fragment, - ReactNode, - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from 'react'; -import { - useCanEnableMApps, - useEnabledMApps, - useEnsAuth, - useIsMAppEnabled, -} from '@justaname.id/react'; -import { AuthorizeMAppDialog } from '../../dialogs/AuthorizeMAppDialog'; -import { PluginProvider } from '../PluginProvider'; -import { JustaPlugin } from '../../plugins'; -import { RevokeMAppDialog } from '../../dialogs/RevokeMAppDialog'; -import { JustWeb3ProviderConfig } from '../../types/config'; - -export interface MApp { - name: string; - isOpen: boolean; -} - -export interface MAppContextProps { - mAppsToEnable: string[] | undefined; - mAppsAlreadyEnabled: string[] | undefined; - canEnableMApps: boolean | undefined; - handleOpenAuthorizeMAppDialog: (mAppName: string, open?: boolean) => void; - handleOpenRevokeMAppDialog: (mAppName: string, open?: boolean) => void; - handleOpenSignInDialog: (open: boolean) => void; - config: JustWeb3ProviderConfig; -} - -export const MAppContext = createContext({ - mAppsToEnable: undefined, - mAppsAlreadyEnabled: undefined, - canEnableMApps: undefined, - handleOpenAuthorizeMAppDialog: () => {}, - handleOpenRevokeMAppDialog: () => {}, - handleOpenSignInDialog: () => {}, - config: {}, -}); - -interface MAppsProviderProps { - logo?: string; - handleOpenSignInDialog: (open: boolean) => void; - children: ReactNode; - mApps?: { - name: string; - openOnConnect: boolean; - }[]; - plugins: JustaPlugin[]; - disableOverlay?: boolean; - config: JustWeb3ProviderConfig; -} - -export const MAppsProvider: FC = ({ - logo, - handleOpenSignInDialog, - children, - disableOverlay, - mApps: initialMApps = [], - plugins, - config, -}) => { - const { isEnsAuthPending, isLoggedIn, connectedEns } = useEnsAuth({ - local: !config.enableAuth, - }); - const [mAppsToEnableOpen, setMAppsToEnableOpen] = useState< - { name: string; isOpen: boolean }[] | undefined - >(undefined); - const [mAppsAlreadyEnabledOpen, setMAppsAlreadyEnabledOpen] = useState< - { name: string; isOpen: boolean }[] | undefined - >(undefined); - - const { canEnableMApps } = useCanEnableMApps({ - ens: connectedEns?.ens || '', - }); - const { enabledMApps } = useEnabledMApps({ - ens: connectedEns?.ens || '', - }); - - const mAppsToEnable = useMemo(() => { - if (!initialMApps || !enabledMApps) { - return undefined; - } - return initialMApps.filter((mApp) => !enabledMApps.includes(mApp.name)); - }, [initialMApps, enabledMApps]); - - const mAppsAlreadyEnabled = useMemo(() => { - if (!initialMApps || !enabledMApps) { - return undefined; - } - return initialMApps.filter((mApp) => enabledMApps.includes(mApp.name)); - }, [initialMApps, enabledMApps]); - - useEffect(() => { - if (!mAppsToEnable) { - return; - } - setMAppsToEnableOpen( - mAppsToEnable.map((mApp) => ({ - name: mApp.name, - isOpen: mApp.openOnConnect, - })) - ); - }, [mAppsToEnable]); - - useEffect(() => { - if (!mAppsAlreadyEnabled) { - return; - } - setMAppsAlreadyEnabledOpen( - mAppsAlreadyEnabled.map((mApp) => ({ name: mApp.name, isOpen: false })) - ); - }, [mAppsAlreadyEnabled]); - - const handleOpenAuthorizeMAppDialog = useCallback( - (mAppName: string, open = true) => { - setMAppsToEnableOpen((prev) => - prev?.map((mApp) => { - if (mApp.name === mAppName) { - return { - ...mApp, - isOpen: open, - }; - } - return mApp; - }) - ); - }, - [] - ); - - const handleOpenRevokeMAppDialog = useCallback( - (mAppName: string, open = true) => { - setMAppsAlreadyEnabledOpen((prev) => - prev?.map((mApp) => { - if (mApp.name === mAppName) { - return { - ...mApp, - isOpen: open, - }; - } - return mApp; - }) - ); - }, - [] - ); - - return ( - mApp.name), - mAppsAlreadyEnabled: mAppsAlreadyEnabled?.map((mApp) => mApp.name), - handleOpenAuthorizeMAppDialog, - handleOpenRevokeMAppDialog, - canEnableMApps, - handleOpenSignInDialog, - config, - }} - > - mApp.name)} - plugins={plugins} - handleOpenSignInDialog={handleOpenSignInDialog} - handleOpenAuthorizeMAppDialog={handleOpenAuthorizeMAppDialog} - handleOpenRevokeMAppDialog={handleOpenRevokeMAppDialog} - config={config} - > - {mAppsToEnableOpen && - mAppsToEnableOpen.map((mApp) => ( - - - handleOpenAuthorizeMAppDialog(mApp.name, open) - } - mApp={mApp} - logo={logo} - handleOpenSignInDialog={handleOpenSignInDialog} - connectedEns={connectedEns?.ens} - isEnsAuthPending={isEnsAuthPending} - isLoggedIn={isLoggedIn} - disableOverlay={disableOverlay} - /> - - ))} - {mAppsAlreadyEnabledOpen && - mAppsAlreadyEnabledOpen.map((mApp) => ( - - - handleOpenRevokeMAppDialog(mApp.name, open) - } - mApp={mApp} - logo={logo} - handleOpenSignInDialog={handleOpenSignInDialog} - connectedEns={connectedEns?.ens} - isEnsAuthPending={isEnsAuthPending} - isLoggedIn={isLoggedIn} - disableOverlay={disableOverlay} - /> - - ))} - {children} - - - ); -}; - -interface UseMAppParams { - mApp: string; -} - -interface UseMAppResult { - handleOpenAuthorizeMAppDialog: (open: boolean) => void; - handleOpenRevokeMAppDialog: (open: boolean) => void; - isMAppEnabled: boolean | undefined; - canOpenMAppDialog: boolean; - isPending: boolean; -} - -export const useMApp = ({ mApp }: UseMAppParams): UseMAppResult => { - const { - handleOpenAuthorizeMAppDialog: contextOpenAuthorizeMAppDialog, - handleOpenRevokeMAppDialog: contextOpenRevokeMAppDialog, - handleOpenSignInDialog, - config, - } = useContext(MAppContext); - const { connectedEns } = useEnsAuth({ - local: !config.enableAuth, - }); - const { isMAppEnabled, isMAppEnabledPending } = useIsMAppEnabled({ - ens: connectedEns?.ens || '', - mApp, - }); - const { canEnableMApps, isCanEnableMAppsPending } = useCanEnableMApps({ - ens: connectedEns?.ens || '', - }); - - const isPending = isMAppEnabledPending || isCanEnableMAppsPending; - - const handleOpenAuthorizeMAppDialog = useCallback( - (open: boolean) => { - if (!connectedEns) { - handleOpenSignInDialog(true); - return; - } - - if (isPending) { - return; - } - - if (!isMAppEnabled && canEnableMApps) { - contextOpenAuthorizeMAppDialog(mApp, open); - } - }, - [ - connectedEns, - handleOpenSignInDialog, - isPending, - isMAppEnabled, - canEnableMApps, - contextOpenAuthorizeMAppDialog, - mApp, - ] - ); - - const handleOpenRevokeMAppDialog = useCallback( - (open: boolean) => { - if (!connectedEns) { - handleOpenSignInDialog(true); - return; - } - - if (isPending) { - return; - } - - if (isMAppEnabled) { - contextOpenRevokeMAppDialog(mApp, open); - } - }, - [ - connectedEns, - handleOpenSignInDialog, - isPending, - isMAppEnabled, - contextOpenRevokeMAppDialog, - mApp, - ] - ); - - const canOpenMAppDialog = useMemo(() => { - if (!connectedEns) { - return true; - } - return !isMAppEnabled && canEnableMApps && !isPending; - }, [connectedEns, isMAppEnabled, canEnableMApps, isPending]); - - return { - handleOpenAuthorizeMAppDialog, - handleOpenRevokeMAppDialog, - isMAppEnabled, - canOpenMAppDialog: Boolean(canOpenMAppDialog), - isPending, - }; -}; - -export const useMApps = (): MAppContextProps => { - const context = useContext(MAppContext); - if (context === undefined) { - throw new Error('useMApps must be used within a MAppsProvider'); - } - return context; -}; diff --git a/packages/@justweb3/widget/src/lib/providers/PluginProvider/index.tsx b/packages/@justweb3/widget/src/lib/providers/PluginProvider/index.tsx index 5e4d8743..cf5cb4c0 100644 --- a/packages/@justweb3/widget/src/lib/providers/PluginProvider/index.tsx +++ b/packages/@justweb3/widget/src/lib/providers/PluginProvider/index.tsx @@ -8,8 +8,6 @@ import React, { useState, } from 'react'; import { - useCanEnableMApps, - useEnabledMApps, useEnsAuth, useMountedAccount, useRecords, @@ -34,9 +32,6 @@ interface PluginProviderProps { children: React.ReactNode; plugins: JustaPlugin[]; handleOpenSignInDialog: (open: boolean) => void; - handleOpenAuthorizeMAppDialog: (mAppName: string, open: boolean) => void; - handleOpenRevokeMAppDialog: (mAppName: string, open: boolean) => void; - mApps?: string[]; config: JustWeb3ProviderConfig; } @@ -44,9 +39,6 @@ export const PluginProvider: FC = ({ children, plugins, handleOpenSignInDialog, - handleOpenAuthorizeMAppDialog, - handleOpenRevokeMAppDialog, - mApps, config, }) => { const { connectedEns, isEnsAuthPending, isLoggedIn } = useEnsAuth({ @@ -55,23 +47,14 @@ export const PluginProvider: FC = ({ const { records } = useRecords({ ens: connectedEns?.ens || '', }); - const { enabledMApps } = useEnabledMApps({ - ens: connectedEns?.ens || '', - }); - const { canEnableMApps } = useCanEnableMApps({ - ens: connectedEns?.ens || '', - }); const { address, chain } = useMountedAccount(); const previousConnectedEns = usePreviousState(connectedEns, [ connectedEns, records, - enabledMApps, - canEnableMApps, ]); const previousChain = usePreviousState(chain, [chain]); const previousAddress = usePreviousState(address, [address]); const previousRecords = usePreviousState(records, [records]); - const previousEnabledMApps = usePreviousState(enabledMApps, [enabledMApps]); const [pluginStates, setPluginStates] = useState< Record> @@ -84,11 +67,8 @@ export const PluginProvider: FC = ({ connectedEns, isEnsAuthPending, isLoggedIn, - mApps: mApps || [], chainId: chain?.id, handleOpenSignInDialog, - handleOpenAuthorizeMAppDialog, - handleOpenRevokeMAppDialog, records, config, eventEmitter: new EventEmitter(), @@ -110,9 +90,6 @@ export const PluginProvider: FC = ({ isEnsAuthPending, isLoggedIn, handleOpenSignInDialog, - handleOpenAuthorizeMAppDialog, - handleOpenRevokeMAppDialog, - mApps, eventEmitter, pluginStates, records, @@ -174,8 +151,6 @@ export const PluginProvider: FC = ({ if ( records && - enabledMApps !== undefined && - canEnableMApps !== undefined && connectedEns && !previousConnectedEns && plugin.hooks?.onEnsSignIn @@ -185,9 +160,7 @@ export const PluginProvider: FC = ({ pluginApi, connectedEns?.ens, chain?.id || 1, - records, - enabledMApps, - canEnableMApps + records ); } catch (error) { console.error( @@ -199,21 +172,13 @@ export const PluginProvider: FC = ({ if ( records && - enabledMApps !== undefined && - canEnableMApps !== undefined && connectedEns && previousConnectedEns && connectedEns.ens !== previousConnectedEns.ens && plugin.hooks?.onEnsChange ) { try { - plugin.hooks?.onEnsChange( - pluginApi, - connectedEns?.ens, - records, - enabledMApps, - canEnableMApps - ); + plugin.hooks?.onEnsChange(pluginApi, connectedEns?.ens, records); } catch (error) { console.error( `Error in plugin ${plugin.name} onEnsChange hook:`, @@ -233,14 +198,7 @@ export const PluginProvider: FC = ({ } } }); - }, [ - plugins, - connectedEns, - previousConnectedEns, - records, - enabledMApps, - canEnableMApps, - ]); + }, [plugins, connectedEns, previousConnectedEns, records]); useEffect(() => { plugins?.forEach((plugin) => { @@ -290,24 +248,13 @@ export const PluginProvider: FC = ({ plugins?.forEach((plugin) => { const pluginApi = createPluginApi(plugin.name); - if ( - !records || - enabledMApps === undefined || - canEnableMApps === undefined || - !connectedEns - ) { + if (!records || !connectedEns) { return; } if (!isEqual(records, previousRecords) && plugin.hooks?.onRecordsChange) { try { - plugin.hooks.onRecordsChange( - pluginApi, - connectedEns?.ens, - records, - enabledMApps, - canEnableMApps - ); + plugin.hooks.onRecordsChange(pluginApi, connectedEns?.ens, records); } catch (error) { console.error( `Error in plugin ${plugin.name} onRecordsChange hook:`, @@ -316,70 +263,7 @@ export const PluginProvider: FC = ({ } } }); - }, [ - plugins, - records, - previousRecords, - connectedEns, - enabledMApps, - canEnableMApps, - ]); - - useEffect(() => { - plugins?.forEach((plugin) => { - const pluginApi = createPluginApi(plugin.name); - - if ( - enabledMApps === undefined || - !connectedEns || - !previousEnabledMApps - ) { - return; - } - - if ( - !isEqual(enabledMApps, previousEnabledMApps) && - plugin.hooks?.onMAppAdd - ) { - const addedMApp = enabledMApps.find( - (mApp) => !previousEnabledMApps.includes(mApp) - ); - if (addedMApp) { - try { - plugin.hooks.onMAppAdd(pluginApi, connectedEns?.ens, addedMApp); - } catch (error) { - console.error( - `Error in plugin ${plugin.name} onMAppAdd hook:`, - error - ); - } - } - } - - if ( - !isEqual(enabledMApps, previousEnabledMApps) && - plugin.hooks?.onMAppRemove - ) { - const removedMApp = previousEnabledMApps.find( - (mApp) => !enabledMApps.includes(mApp) - ); - if (removedMApp) { - try { - plugin.hooks.onMAppRemove( - pluginApi, - connectedEns?.ens, - removedMApp - ); - } catch (error) { - console.error( - `Error in plugin ${plugin.name} onMAppRemove hook:`, - error - ); - } - } - } - }); - }, [plugins, enabledMApps, previousEnabledMApps, connectedEns]); + }, [plugins, records, previousRecords, connectedEns]); const globalComponentsArray: ReactNode[] = plugins?.reduce( (acc: ReactNode[], plugin: JustaPlugin) => { diff --git a/packages/@justweb3/widget/src/lib/providers/index.ts b/packages/@justweb3/widget/src/lib/providers/index.ts index ff941cf6..7250913f 100644 --- a/packages/@justweb3/widget/src/lib/providers/index.ts +++ b/packages/@justweb3/widget/src/lib/providers/index.ts @@ -1,3 +1,2 @@ export * from './JustWeb3Provider' -export { useMApps, useMApp } from './MAppProvider' export { usePlugins } from './PluginProvider' \ No newline at end of file diff --git a/packages/@justweb3/widget/src/lib/types/config/index.ts b/packages/@justweb3/widget/src/lib/types/config/index.ts index a35595c8..a1b4f569 100644 --- a/packages/@justweb3/widget/src/lib/types/config/index.ts +++ b/packages/@justweb3/widget/src/lib/types/config/index.ts @@ -10,7 +10,6 @@ export interface JustWeb3ProviderConfig logo?: string; disableOverlay?: boolean; enableAuth?: boolean; - mApps?: (string | { name: string; openOnConnect: boolean })[]; plugins?: JustaPlugin[]; onLogout?: () => void; } diff --git a/packages/@justweb3/widget/src/stories/multichain.stories.tsx b/packages/@justweb3/widget/src/stories/multichain.stories.tsx index 8378dcd0..82f50f25 100644 --- a/packages/@justweb3/widget/src/stories/multichain.stories.tsx +++ b/packages/@justweb3/widget/src/stories/multichain.stories.tsx @@ -45,7 +45,6 @@ const JustWeb3Config: JustWeb3ProviderConfig = { providerUrl: import.meta.env.STORYBOOK_APP_SEPOLIA_PROVIDER_URL, }, ], - mApps: ['justverified.eth', 'justweb3.eth'], openOnWalletConnect: false, allowedEns: 'all', dev: import.meta.env.STORYBOOK_APP_DEV === 'true', diff --git a/packages/@justweb3/widget/src/stories/signin.stories.tsx b/packages/@justweb3/widget/src/stories/signin.stories.tsx index 36d52c6d..2eaf3931 100644 --- a/packages/@justweb3/widget/src/stories/signin.stories.tsx +++ b/packages/@justweb3/widget/src/stories/signin.stories.tsx @@ -37,7 +37,6 @@ const JustWeb3Config: JustWeb3ProviderConfig = { providerUrl: import.meta.env.STORYBOOK_APP_SEPOLIA_PROVIDER_URL, }, ], - mApps: ['justverified.eth', 'justweb3.eth'], openOnWalletConnect: false, allowedEns: 'all', // dev: import.meta.env.STORYBOOK_APP_DEV === 'true', diff --git a/packages/@justweb3/xmtp-plugin/package.json b/packages/@justweb3/xmtp-plugin/package.json index 686523be..2a66d7a6 100644 --- a/packages/@justweb3/xmtp-plugin/package.json +++ b/packages/@justweb3/xmtp-plugin/package.json @@ -18,6 +18,7 @@ "@justweb3/widget": ">=0.0.95", "@tanstack/react-query": "^5.x", "react": ">=17", + "viem": "^2.48.0", "wagmi": "2.x" }, "exports": { diff --git a/packages/@justweb3/xmtp-plugin/src/lib/hooks/useClient/index.ts b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useClient/index.ts index cded1a18..b5e40845 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/hooks/useClient/index.ts +++ b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useClient/index.ts @@ -1,18 +1,16 @@ 'use client'; -import { arrayify } from '@ethersproject/bytes'; import { Client, type Signer } from '@xmtp/browser-sdk'; import { ReactionCodec } from '@xmtp/content-type-reaction'; import { AttachmentCodec } from '@xmtp/content-type-remote-attachment'; import { ReplyCodec } from '@xmtp/content-type-reply'; -import { JsonRpcSigner } from 'ethers'; import { useCallback, useContext, useRef, useState } from 'react'; import { XMTPContext } from '../../contexts/XMTPContext'; import { ReadReceiptCodec } from '@xmtp/content-type-read-receipt'; import { useAccount } from 'wagmi'; export type InitializeClientOptions = { - signer: JsonRpcSigner; + signer: Signer; }; function storeKeys(address: string, key: Uint8Array, env: string) { @@ -61,23 +59,7 @@ export const useXMTPClient = (onError?: (error: Error) => void) => { storeKeys(address ?? '', encryptionKey, env); } - // Create XMTP signer that converts ethers signatures to Uint8Array - const xmtpSigner: Signer = { - type: 'EOA', - getIdentifier: async () => { - const signerAddress = await signer.getAddress(); - return { - identifier: signerAddress, - identifierKind: 'Ethereum', - }; - }, - signMessage: async (message: string) => { - const signature = await signer.signMessage(message); - return arrayify(signature); - }, - }; - - xmtpClient = await Client.create(xmtpSigner, { + xmtpClient = await Client.create(signer, { dbEncryptionKey: encryptionKey, env, loggingLevel: diff --git a/packages/@justweb3/xmtp-plugin/src/lib/hooks/useEthersSigner/index.ts b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useEthersSigner/index.ts index d12456cf..3fd572fe 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/hooks/useEthersSigner/index.ts +++ b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useEthersSigner/index.ts @@ -1,30 +1,41 @@ 'use client'; -import { BrowserProvider, JsonRpcSigner } from 'ethers'; +import type { Signer } from '@xmtp/browser-sdk'; import { useMemo } from 'react'; -import type { Account, Chain, Client, Transport } from 'viem'; +import { hexToBytes } from 'viem'; import { useWalletClient } from 'wagmi'; -export function clientToSigner(client: Client) { - const { account, chain, transport } = client; - if (!account || !chain || !transport) return undefined; - const network = { - chainId: chain?.id, - name: chain?.name, - ensAddress: chain?.contracts?.ensRegistry?.address, - }; - const provider = new BrowserProvider(transport, network); - const signer = new JsonRpcSigner(provider, account.address); - return signer; -} +/** + * Returns an XMTP-compatible `Signer` backed by the wagmi/viem `WalletClient`. + * + * Renamed from `useEthersSigner` once the SDK dropped ethers in favor of viem. + * Old name kept as alias for backwards compatibility within this plugin. + */ +export const useXmtpSigner = ({ + chainId, +}: { chainId?: number } = {}): Signer | undefined => { + const { data: walletClient } = useWalletClient({ chainId }); -/** Hook to convert a viem Wallet Client to an ethers.js Signer. */ -export const useEthersSigner = ({ chainId }: { chainId?: number } = {}): - | JsonRpcSigner - | undefined => { - const { data: client } = useWalletClient({ chainId }); - const signer = useMemo( - () => (client ? clientToSigner(client) : undefined), - [client] - ); - return signer; + return useMemo(() => { + if (!walletClient?.account) return undefined; + const account = walletClient.account; + return { + type: 'EOA', + getIdentifier: async () => ({ + identifier: account.address, + identifierKind: 'Ethereum', + }), + signMessage: async (message: string) => { + const signature = await walletClient.signMessage({ + account, + message, + }); + return hexToBytes(signature); + }, + }; + }, [walletClient]); }; + +/** + * @deprecated Renamed to `useXmtpSigner`. This alias will be removed in the next major version. + */ +export const useEthersSigner = useXmtpSigner; diff --git a/packages/siwens/package.json b/packages/siwens/package.json index 52de44cc..29ad08cf 100644 --- a/packages/siwens/package.json +++ b/packages/siwens/package.json @@ -1,9 +1,12 @@ { "name": "siwens", "version": "0.1.52", + "dependencies": { + "punycode": "^2.3.1" + }, "peerDependencies": { - "ethers": "^5.6.8 || ^6.0.8", - "siwe": ">=2.0.0" + "siwe": ">=2.0.0", + "viem": "^2.48.0" }, "exports": { "./package.json": "./dist/package.json", diff --git a/yarn.lock b/yarn.lock index 7d3466b7..d9a439d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4472,9 +4472,8 @@ __metadata: qs: "npm:6.12.0" peerDependencies: "@tanstack/react-query": ^5.x - ethers: ^5.6.8 || ^6.0.8 react: ">=17" - viem: 2.x + viem: ^2.48.0 wagmi: 2.x languageName: unknown linkType: soft @@ -4488,18 +4487,19 @@ __metadata: jest: "npm:^29.4.1" qs: "npm:6.12.0" peerDependencies: - ethers: ^5.6.8 || ^6.0.8 siwe: ">=2.0.0" - viem: ">=2.35.0" + viem: ^2.48.0 languageName: unknown linkType: soft "@justaname.id/siwens@npm:0.0.145, @justaname.id/siwens@workspace:packages/@justaname.id/siwens": version: 0.0.0-use.local resolution: "@justaname.id/siwens@workspace:packages/@justaname.id/siwens" + dependencies: + punycode: "npm:^2.3.1" peerDependencies: - ethers: ^5.6.8 || ^6.0.8 siwe: ">=2.0.0" + viem: ^2.48.0 languageName: unknown linkType: soft @@ -4628,6 +4628,7 @@ __metadata: "@justweb3/widget": ">=0.0.95" "@tanstack/react-query": ^5.x react: ">=17" + viem: ^2.48.0 wagmi: 2.x languageName: unknown linkType: soft @@ -5564,15 +5565,6 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:1.2.0": - version: 1.2.0 - resolution: "@noble/curves@npm:1.2.0" - dependencies: - "@noble/hashes": "npm:1.3.2" - checksum: 10c0/0bac7d1bbfb3c2286910b02598addd33243cb97c3f36f987ecc927a4be8d7d88e0fcb12b0f0ef8a044e7307d1844dd5c49bb724bfa0a79c8ec50ba60768c97f6 - languageName: node - linkType: hard - "@noble/curves@npm:1.4.2, @noble/curves@npm:~1.4.0": version: 1.4.2 resolution: "@noble/curves@npm:1.4.2" @@ -5634,13 +5626,6 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.3.2": - version: 1.3.2 - resolution: "@noble/hashes@npm:1.3.2" - checksum: 10c0/2482cce3bce6a596626f94ca296e21378e7a5d4c09597cbc46e65ffacc3d64c8df73111f2265444e36a3168208628258bbbaccba2ef24f65f58b2417638a20e7 - languageName: node - linkType: hard - "@noble/hashes@npm:1.4.0, @noble/hashes@npm:~1.4.0": version: 1.4.0 resolution: "@noble/hashes@npm:1.4.0" @@ -11646,13 +11631,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:18.15.13": - version: 18.15.13 - resolution: "@types/node@npm:18.15.13" - checksum: 10c0/6e5f61c559e60670a7a8fb88e31226ecc18a21be103297ca4cf9848f0a99049dae77f04b7ae677205f2af494f3701b113ba8734f4b636b355477a6534dbb8ada - languageName: node - linkType: hard - "@types/node@npm:18.16.9": version: 18.16.9 resolution: "@types/node@npm:18.16.9" @@ -13940,9 +13918,9 @@ __metadata: languageName: node linkType: hard -"abitype@npm:1.1.0": - version: 1.1.0 - resolution: "abitype@npm:1.1.0" +"abitype@npm:1.2.3": + version: 1.2.3 + resolution: "abitype@npm:1.2.3" peerDependencies: typescript: ">=5.0.4" zod: ^3.22.0 || ^4.0.0 @@ -13951,13 +13929,13 @@ __metadata: optional: true zod: optional: true - checksum: 10c0/99218d442951c60324fcd96a372c30d71ca8d5434cab62b95d5d80bae89e3024a445a90db323ef1fe4da0d749d86e815ca555a37719b06e6ca03ccad2116c45b + checksum: 10c0/c8740de1ae4961723a153224a52cb9a34a57903fb5c2ad61d5082b0b79b53033c9335381aa8c663c7ec213c9955a9853f694d51e95baceedef27356f7745c634 languageName: node linkType: hard -"abitype@npm:^1.0.9": - version: 1.1.1 - resolution: "abitype@npm:1.1.1" +"abitype@npm:^1.2.3": + version: 1.2.4 + resolution: "abitype@npm:1.2.4" peerDependencies: typescript: ">=5.0.4" zod: ^3.22.0 || ^4.0.0 @@ -13966,7 +13944,7 @@ __metadata: optional: true zod: optional: true - checksum: 10c0/d52fd8195cb37cdb462ba4d1817dafdba8da403eeab50f144f251748d7458a43308ee29ea46889db2969c91c074780e6d1f00f86acd22dc5772570432ee56b9c + checksum: 10c0/b420d8368f92a9bf456bc51a15866af2d8463e2397006551148e654cef9ca786a31d487e27942992c5b5b443b8a6b8adb0efff0c96d58e2ed81b23940fe86b2f languageName: node linkType: hard @@ -14096,13 +14074,6 @@ __metadata: languageName: node linkType: hard -"aes-js@npm:4.0.0-beta.5": - version: 4.0.0-beta.5 - resolution: "aes-js@npm:4.0.0-beta.5" - checksum: 10c0/444f4eefa1e602cbc4f2a3c644bc990f93fd982b148425fee17634da510586fc09da940dcf8ace1b2d001453c07ff042e55f7a0482b3cc9372bf1ef75479090c - languageName: node - linkType: hard - "agent-base@npm:6": version: 6.0.2 resolution: "agent-base@npm:6.0.2" @@ -19977,21 +19948,6 @@ __metadata: languageName: node linkType: hard -"ethers@npm:6.11.1": - version: 6.11.1 - resolution: "ethers@npm:6.11.1" - dependencies: - "@adraffy/ens-normalize": "npm:1.10.1" - "@noble/curves": "npm:1.2.0" - "@noble/hashes": "npm:1.3.2" - "@types/node": "npm:18.15.13" - aes-js: "npm:4.0.0-beta.5" - tslib: "npm:2.4.0" - ws: "npm:8.5.0" - checksum: 10c0/97a920e0244ba6cd1622b58a448c87f26dad20bad242777abb2e583d045bf7752218477bd7367ba6518c2a5e2b16030afff15e87b705526d0ea667498c27ac89 - languageName: node - linkType: hard - "ethers@npm:^5.7.2": version: 5.8.0 resolution: "ethers@npm:5.8.0" @@ -25436,7 +25392,6 @@ __metadata: eslint-plugin-jsx-a11y: "npm:6.7.1" eslint-plugin-react: "npm:7.32.2" eslint-plugin-react-hooks: "npm:4.6.0" - ethers: "npm:6.11.1" express: "npm:4.18.1" express-session: "npm:1.18.0" input-otp: "npm:1.2.4" @@ -25486,7 +25441,7 @@ __metadata: url-loader: "npm:4.1.1" vaul: "npm:1.1.1" verdaccio: "npm:5.0.4" - viem: "npm:^2.35.0" + viem: "npm:^2.48.0" vite: "npm:~5.0.0" vite-plugin-dts: "npm:3.7.3" vite-plugin-node-polyfills: "npm:0.22.0" @@ -28517,6 +28472,27 @@ __metadata: languageName: node linkType: hard +"ox@npm:0.14.20": + version: 0.14.20 + resolution: "ox@npm:0.14.20" + dependencies: + "@adraffy/ens-normalize": "npm:^1.11.0" + "@noble/ciphers": "npm:^1.3.0" + "@noble/curves": "npm:1.9.1" + "@noble/hashes": "npm:^1.8.0" + "@scure/bip32": "npm:^1.7.0" + "@scure/bip39": "npm:^1.6.0" + abitype: "npm:^1.2.3" + eventemitter3: "npm:5.0.1" + peerDependencies: + typescript: ">=5.4.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/fe1c34577536aea3bda5ec47e083be9069d99cb35a270527921f1c7b406283f12bc4b7e9348b46aa3ba88555fc8abc963129c6b5b8b3b375d5e1549f65f2ce9a + languageName: node + linkType: hard + "ox@npm:0.6.7": version: 0.6.7 resolution: "ox@npm:0.6.7" @@ -28557,27 +28533,6 @@ __metadata: languageName: node linkType: hard -"ox@npm:0.9.6": - version: 0.9.6 - resolution: "ox@npm:0.9.6" - dependencies: - "@adraffy/ens-normalize": "npm:^1.11.0" - "@noble/ciphers": "npm:^1.3.0" - "@noble/curves": "npm:1.9.1" - "@noble/hashes": "npm:^1.8.0" - "@scure/bip32": "npm:^1.7.0" - "@scure/bip39": "npm:^1.6.0" - abitype: "npm:^1.0.9" - eventemitter3: "npm:5.0.1" - peerDependencies: - typescript: ">=5.4.0" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10c0/559b39051f80a25352e1ca6e7aba6e04f60c4e29f98e4ef3ec0c8d2b0432d400004ce09d2991200eaf21745179af47367dc28c553da43403dd0b69c2453ebabe - languageName: node - linkType: hard - "p-cancelable@npm:^2.0.0": version: 2.1.1 resolution: "p-cancelable@npm:2.1.1" @@ -35080,13 +35035,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.4.0": - version: 2.4.0 - resolution: "tslib@npm:2.4.0" - checksum: 10c0/eb19bda3ae545b03caea6a244b34593468e23d53b26bf8649fbc20fce43e9b21a71127fd6d2b9662c0fe48ee6ff668ead48fd00d3b88b2b716b1c12edae25b5d - languageName: node - linkType: hard - "tslib@npm:2.6.2": version: 2.6.2 resolution: "tslib@npm:2.6.2" @@ -36320,24 +36268,24 @@ __metadata: languageName: node linkType: hard -"viem@npm:^2.35.0": - version: 2.38.6 - resolution: "viem@npm:2.38.6" +"viem@npm:^2.48.0": + version: 2.49.3 + resolution: "viem@npm:2.49.3" dependencies: "@noble/curves": "npm:1.9.1" "@noble/hashes": "npm:1.8.0" "@scure/bip32": "npm:1.7.0" "@scure/bip39": "npm:1.6.0" - abitype: "npm:1.1.0" + abitype: "npm:1.2.3" isows: "npm:1.0.7" - ox: "npm:0.9.6" + ox: "npm:0.14.20" ws: "npm:8.18.3" peerDependencies: typescript: ">=5.0.4" peerDependenciesMeta: typescript: optional: true - checksum: 10c0/9b8571bc9d7dfc414eb72700275ac71b9402cbf4fe3e447501d252cbc43cb7ddf9ce01a16c183fac265d071873d209efc2a459462c724624ef855d941d3447f0 + checksum: 10c0/f98d0601ec6fecbca0e223735ea59694ac6c6925f85e64b00e46ec57bb1f59c3c5efc84a52abc372875ec730c35254ce6a9cad313dfdcc151ffd6b5d45102a4c languageName: node linkType: hard @@ -37423,21 +37371,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:8.5.0": - version: 8.5.0 - resolution: "ws@npm:8.5.0" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10c0/0baeee03e97865accda8fad51e8e5fa17d19b8e264529efdf662bbba2acc1c7f1de8316287e6df5cb639231a96009e6d5234b57e6ff36ee2d04e49a0995fec2f - languageName: node - linkType: hard - "ws@npm:^7.0.0, ws@npm:^7.5.1": version: 7.5.10 resolution: "ws@npm:7.5.10" From 4ab6cc6175c360a13a858bcb2219532cd59712f6 Mon Sep 17 00:00:00 2001 From: anthony Date: Wed, 17 Jun 2026 14:23:07 +0300 Subject: [PATCH 2/3] fix: posthog enabled, enhanced events --- apps/console/next.config.js | 54 +++++++------------ .../claimSection/ens-by-api-key-called.ts | 7 ++- .../src/analytics/events/code/code-copied.ts | 9 +++- .../navigation/dashboard-link-clicked.ts | 3 -- .../events/navigation/docs-link-clicked.ts | 3 -- .../src/analytics/events/navigation/index.ts | 12 ++--- .../events/navigation/link-clicked.ts | 10 ++++ .../events/navigation/profile-viewed.ts | 2 + .../events/network/network-changed.ts | 4 ++ .../events/plugins/dentity-disabled.ts | 3 -- .../events/plugins/dentity-enabled.ts | 3 -- .../analytics/events/plugins/efp-disabled.ts | 3 -- .../analytics/events/plugins/efp-enabled.ts | 3 -- .../src/analytics/events/plugins/index.ts | 50 ++++------------- .../events/plugins/just-verified-disabled.ts | 3 -- .../events/plugins/just-verified-enabled.ts | 3 -- .../plugins/justVerified/discord-disabled.ts | 3 -- .../plugins/justVerified/discord-enabled.ts | 3 -- .../plugins/justVerified/email-disabled.ts | 3 -- .../plugins/justVerified/email-enabled.ts | 3 -- .../plugins/justVerified/github-disabled.ts | 3 -- .../plugins/justVerified/github-enabled.ts | 3 -- .../events/plugins/justVerified/index.ts | 39 -------------- .../plugins/justVerified/telegram-disabled.ts | 3 -- .../plugins/justVerified/telegram-enabled.ts | 3 -- .../plugins/justVerified/twitter-disabled.ts | 3 -- .../plugins/justVerified/twitter-enabled.ts | 3 -- .../events/plugins/plugin-toggled.ts | 13 +++++ .../analytics/events/plugins/poap-disabled.ts | 3 -- .../analytics/events/plugins/poap-enabled.ts | 3 -- .../events/plugins/verification-toggled.ts | 13 +++++ .../analytics/events/plugins/xmtp-disabled.ts | 3 -- .../analytics/events/plugins/xmtp-enabled.ts | 3 -- .../events/signSection/any-ens-selected.ts | 3 -- .../signSection/claimable-ens-selected.ts | 3 -- .../signSection/ens-selection-changed.ts | 10 ++++ .../src/analytics/events/signSection/index.ts | 19 ++----- .../signSection/specific-ens-selected.ts | 5 -- apps/console/src/analytics/index.ts | 15 +++++- apps/console/src/app/page.tsx | 1 + .../sections/code/CodeSection/index.tsx | 9 +++- .../customizer/ClaimSection/index.tsx | 13 +++-- .../sections/customizer/Customizer/index.tsx | 1 + .../PluginsSection/Dentity/index.tsx | 10 +++- .../customizer/PluginsSection/EFP/index.tsx | 10 +++- .../PluginsSection/JustVerified/index.tsx | 36 ++++++------- .../customizer/PluginsSection/POAP/index.tsx | 10 +++- .../customizer/PluginsSection/XMTP/index.tsx | 10 +++- .../sections/customizer/SignSection/index.tsx | 11 ++-- apps/console/src/layout/navbar/index.tsx | 7 ++- 50 files changed, 198 insertions(+), 254 deletions(-) delete mode 100644 apps/console/src/analytics/events/navigation/dashboard-link-clicked.ts delete mode 100644 apps/console/src/analytics/events/navigation/docs-link-clicked.ts create mode 100644 apps/console/src/analytics/events/navigation/link-clicked.ts delete mode 100644 apps/console/src/analytics/events/plugins/dentity-disabled.ts delete mode 100644 apps/console/src/analytics/events/plugins/dentity-enabled.ts delete mode 100644 apps/console/src/analytics/events/plugins/efp-disabled.ts delete mode 100644 apps/console/src/analytics/events/plugins/efp-enabled.ts delete mode 100644 apps/console/src/analytics/events/plugins/just-verified-disabled.ts delete mode 100644 apps/console/src/analytics/events/plugins/just-verified-enabled.ts delete mode 100644 apps/console/src/analytics/events/plugins/justVerified/discord-disabled.ts delete mode 100644 apps/console/src/analytics/events/plugins/justVerified/discord-enabled.ts delete mode 100644 apps/console/src/analytics/events/plugins/justVerified/email-disabled.ts delete mode 100644 apps/console/src/analytics/events/plugins/justVerified/email-enabled.ts delete mode 100644 apps/console/src/analytics/events/plugins/justVerified/github-disabled.ts delete mode 100644 apps/console/src/analytics/events/plugins/justVerified/github-enabled.ts delete mode 100644 apps/console/src/analytics/events/plugins/justVerified/index.ts delete mode 100644 apps/console/src/analytics/events/plugins/justVerified/telegram-disabled.ts delete mode 100644 apps/console/src/analytics/events/plugins/justVerified/telegram-enabled.ts delete mode 100644 apps/console/src/analytics/events/plugins/justVerified/twitter-disabled.ts delete mode 100644 apps/console/src/analytics/events/plugins/justVerified/twitter-enabled.ts create mode 100644 apps/console/src/analytics/events/plugins/plugin-toggled.ts delete mode 100644 apps/console/src/analytics/events/plugins/poap-disabled.ts delete mode 100644 apps/console/src/analytics/events/plugins/poap-enabled.ts create mode 100644 apps/console/src/analytics/events/plugins/verification-toggled.ts delete mode 100644 apps/console/src/analytics/events/plugins/xmtp-disabled.ts delete mode 100644 apps/console/src/analytics/events/plugins/xmtp-enabled.ts delete mode 100644 apps/console/src/analytics/events/signSection/any-ens-selected.ts delete mode 100644 apps/console/src/analytics/events/signSection/claimable-ens-selected.ts create mode 100644 apps/console/src/analytics/events/signSection/ens-selection-changed.ts delete mode 100644 apps/console/src/analytics/events/signSection/specific-ens-selected.ts diff --git a/apps/console/next.config.js b/apps/console/next.config.js index 5f753952..b6ed75a7 100644 --- a/apps/console/next.config.js +++ b/apps/console/next.config.js @@ -15,51 +15,37 @@ const nextConfig = { svgr: false, }, async rewrites() { + // PostHog reverse proxy (EU region). posthog-js is configured with + // api_host: '/analytics' so the browser only ever hits this first-party + // path (ad-blocker resistant); Next forwards server-side to PostHog. + // + // IMPORTANT: PostHog retired `eu.posthog.com` for ingestion. Static assets + // must go to `eu-assets.i.posthog.com` and everything else (event capture, + // /decide, session recording /s) to `eu.i.posthog.com`. The dedicated + // ingestion subdomain is the supported, durable target. return [ - // Posthog + // Static assets (array.js, recorder.js, ...) -> assets host. { - source: '/analytics/:path*', - destination: 'https://eu.posthog.com/:path*', + source: '/analytics/static/:path*', + destination: 'https://eu-assets.i.posthog.com/static/:path*', }, + // Event ingestion / decide / session recording -> ingestion host. + // posthog-js sends to `/e/`, `/decide/`, `/s/` WITH a trailing slash, so + // both the trailing-slash and bare variants are required (a single + // `:path*` rule does not match `/analytics/e/`). { source: '/analytics/:path*/', - destination: 'https://eu.posthog.com/:path*/', + destination: 'https://eu.i.posthog.com/:path*/', }, - ]; - }, - skipTrailingSlashRedirect: true, - async headers() { - async function getMyIp() { - const x = await fetch('https://api.ipify.org'); - // const x = await fetch('https://api.my-ip.io/ip') - return await x.text(); - } - const ip = await getMyIp(); - return [ { source: '/analytics/:path*', - headers: [ - { key: 'X-Forwarded-Proto', value: 'https' }, - { - key: 'X-Forwarded-Host', - value: 'https://www.useflytrap.com', - }, - { key: 'X-Forwarded-For', value: ip }, - ], - }, - { - source: '/analytics/:path*/', - headers: [ - { key: 'X-Forwarded-Proto', value: 'https' }, - { - key: 'X-Forwarded-Host', - value: 'https://www.useflytrap.com', - }, - { key: 'X-Forwarded-For', value: ip }, - ], + destination: 'https://eu.i.posthog.com/:path*', }, ]; }, + // Prevent Next from 308-redirecting `/analytics/e/` -> `/analytics/e`, which + // would strip the trailing slash posthog-js depends on. + skipTrailingSlashRedirect: true, experimental: { serverComponentsExternalPackages: ['@xmtp/user-preferences-bindings-wasm'], }, diff --git a/apps/console/src/analytics/events/claimSection/ens-by-api-key-called.ts b/apps/console/src/analytics/events/claimSection/ens-by-api-key-called.ts index 8508bebe..3ae3dcc7 100644 --- a/apps/console/src/analytics/events/claimSection/ens-by-api-key-called.ts +++ b/apps/console/src/analytics/events/claimSection/ens-by-api-key-called.ts @@ -1,3 +1,8 @@ export const ENS_BY_API_KEY_CALLED = 'ENS_BY_API_KEY_CALLED'; -export interface EnsByApiKeyCalledPayload {} +export interface EnsByApiKeyCalledPayload { + /** Where in the UI the lookup was triggered (e.g. 'claim_section'). */ + location: string; + /** Number of domains returned for the supplied API key. */ + domainCount: number; +} diff --git a/apps/console/src/analytics/events/code/code-copied.ts b/apps/console/src/analytics/events/code/code-copied.ts index d0ce9396..9a0d43b4 100644 --- a/apps/console/src/analytics/events/code/code-copied.ts +++ b/apps/console/src/analytics/events/code/code-copied.ts @@ -1,3 +1,10 @@ export const CODE_COPIED = 'CODE_COPIED'; -export interface CodeCopiedPayload {} +export type CodeSnippet = 'integration' | 'dependencies'; + +export interface CodeCopiedPayload { + /** Where in the UI the copy happened (e.g. 'code_section'). */ + location: string; + /** Which snippet was copied: the integration code or the install command. */ + snippet: CodeSnippet; +} diff --git a/apps/console/src/analytics/events/navigation/dashboard-link-clicked.ts b/apps/console/src/analytics/events/navigation/dashboard-link-clicked.ts deleted file mode 100644 index 8fdbd749..00000000 --- a/apps/console/src/analytics/events/navigation/dashboard-link-clicked.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const DASHBOARD_LINK_CLICKED = 'DASHBOARD_LINK_CLICKED'; - -export interface DashboardLinkClickedPayload {} diff --git a/apps/console/src/analytics/events/navigation/docs-link-clicked.ts b/apps/console/src/analytics/events/navigation/docs-link-clicked.ts deleted file mode 100644 index 04f2f392..00000000 --- a/apps/console/src/analytics/events/navigation/docs-link-clicked.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const DOCS_LINK_CLICKED = 'DOCS_LINK_CLICKED'; - -export interface DocsLinkClickedPayload {} diff --git a/apps/console/src/analytics/events/navigation/index.ts b/apps/console/src/analytics/events/navigation/index.ts index bc695925..5c6e24cb 100644 --- a/apps/console/src/analytics/events/navigation/index.ts +++ b/apps/console/src/analytics/events/navigation/index.ts @@ -1,18 +1,12 @@ -import { - DASHBOARD_LINK_CLICKED, - DashboardLinkClickedPayload, -} from './dashboard-link-clicked'; -import { DOCS_LINK_CLICKED, DocsLinkClickedPayload } from './docs-link-clicked'; +import { LINK_CLICKED, LinkClickedPayload } from './link-clicked'; import { PROFILE_VIEWED, ProfileViewedPayload } from './profile-viewed'; export const NAVIGATION_EVENTS = { - DOCS_LINK_CLICKED, + LINK_CLICKED, PROFILE_VIEWED, - DASHBOARD_LINK_CLICKED, } as const; export interface NavigationEventPayload { - [DOCS_LINK_CLICKED]: DocsLinkClickedPayload; + [LINK_CLICKED]: LinkClickedPayload; [PROFILE_VIEWED]: ProfileViewedPayload; - [DASHBOARD_LINK_CLICKED]: DashboardLinkClickedPayload; } diff --git a/apps/console/src/analytics/events/navigation/link-clicked.ts b/apps/console/src/analytics/events/navigation/link-clicked.ts new file mode 100644 index 00000000..762a8ddf --- /dev/null +++ b/apps/console/src/analytics/events/navigation/link-clicked.ts @@ -0,0 +1,10 @@ +export const LINK_CLICKED = 'LINK_CLICKED'; + +export type LinkTarget = 'docs' | 'dashboard'; + +export interface LinkClickedPayload { + /** Which external destination the user navigated to. */ + target: LinkTarget; + /** Where in the UI the link lives (e.g. 'navbar', 'claim_section'). */ + location: string; +} diff --git a/apps/console/src/analytics/events/navigation/profile-viewed.ts b/apps/console/src/analytics/events/navigation/profile-viewed.ts index dec1398a..ec06f1aa 100644 --- a/apps/console/src/analytics/events/navigation/profile-viewed.ts +++ b/apps/console/src/analytics/events/navigation/profile-viewed.ts @@ -2,5 +2,7 @@ export const PROFILE_VIEWED = 'PROFILE_VIEWED'; export interface ProfileViewedPayload { ens: string; + /** Where the profile was opened from (e.g. 'demo_card'). */ + location: string; chainId?: number; } diff --git a/apps/console/src/analytics/events/network/network-changed.ts b/apps/console/src/analytics/events/network/network-changed.ts index 6af14f5e..6e2197a7 100644 --- a/apps/console/src/analytics/events/network/network-changed.ts +++ b/apps/console/src/analytics/events/network/network-changed.ts @@ -1,5 +1,9 @@ export const NETWORK_CHANGED = 'NETWORK_CHANGED'; +export type NetworkName = 'mainnet' | 'sepolia'; + export interface NetworkChangedPayload { chainId: number; + /** Human-readable network the user switched to. */ + network: NetworkName; } diff --git a/apps/console/src/analytics/events/plugins/dentity-disabled.ts b/apps/console/src/analytics/events/plugins/dentity-disabled.ts deleted file mode 100644 index 22298504..00000000 --- a/apps/console/src/analytics/events/plugins/dentity-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const DENTITY_DISABLED = 'DENTITY_DISABLED'; - -export interface DentityDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/dentity-enabled.ts b/apps/console/src/analytics/events/plugins/dentity-enabled.ts deleted file mode 100644 index 0daf4dc6..00000000 --- a/apps/console/src/analytics/events/plugins/dentity-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const DENTITY_ENABLED = 'DENTITY_ENABLED'; - -export interface DentityEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/efp-disabled.ts b/apps/console/src/analytics/events/plugins/efp-disabled.ts deleted file mode 100644 index adc5b174..00000000 --- a/apps/console/src/analytics/events/plugins/efp-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const EFP_DISABLED = 'EFP_DISABLED'; - -export interface EfpDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/efp-enabled.ts b/apps/console/src/analytics/events/plugins/efp-enabled.ts deleted file mode 100644 index 2c1819db..00000000 --- a/apps/console/src/analytics/events/plugins/efp-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const EFP_ENABLED = 'EFP_ENABLED'; - -export interface EfpEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/index.ts b/apps/console/src/analytics/events/plugins/index.ts index 26b2de7d..ca32fba8 100644 --- a/apps/console/src/analytics/events/plugins/index.ts +++ b/apps/console/src/analytics/events/plugins/index.ts @@ -1,47 +1,15 @@ -import { DENTITY_ENABLED, DentityEnabledPayload } from './dentity-enabled'; -import { DENTITY_DISABLED, DentityDisabledPayload } from './dentity-disabled'; -import { EFP_DISABLED, EfpDisabledPayload } from './efp-disabled'; -import { EFP_ENABLED, EfpEnabledPayload } from './efp-enabled'; +import { PLUGIN_TOGGLED, PluginToggledPayload } from './plugin-toggled'; import { - JUST_VERIFIED_DISABLED, - JustVerifiedDisabledPayload, -} from './just-verified-disabled'; -import { - JUST_VERIFIED_ENABLED, - JustVerifiedEnabledPayload, -} from './just-verified-enabled'; -import { - JUST_VERIFIED_EVENTS, - JustVerifiedEventsPayload, -} from './justVerified'; -import { POAP_DISABLED, PoapDisabledPayload } from './poap-disabled'; -import { POAP_ENABLED, PoapEnabledPayload } from './poap-enabled'; -import { XMTP_DISABLED, XmtpDisabledPayload } from './xmtp-disabled'; -import { XMTP_ENABLED, XmtpEnabledPayload } from './xmtp-enabled'; + VERIFICATION_TOGGLED, + VerificationToggledPayload, +} from './verification-toggled'; export const PLUGINS_EVENTS = { - JUST_VERIFIED_DISABLED, - JUST_VERIFIED_ENABLED, - EFP_DISABLED, - EFP_ENABLED, - POAP_DISABLED, - POAP_ENABLED, - XMTP_DISABLED, - XMTP_ENABLED, - DENTITY_DISABLED, - DENTITY_ENABLED, - ...JUST_VERIFIED_EVENTS, + PLUGIN_TOGGLED, + VERIFICATION_TOGGLED, } as const; -export interface PluginsEventPayload extends JustVerifiedEventsPayload { - [JUST_VERIFIED_DISABLED]: JustVerifiedDisabledPayload; - [JUST_VERIFIED_ENABLED]: JustVerifiedEnabledPayload; - [EFP_DISABLED]: EfpDisabledPayload; - [EFP_ENABLED]: EfpEnabledPayload; - [POAP_DISABLED]: PoapDisabledPayload; - [POAP_ENABLED]: PoapEnabledPayload; - [XMTP_DISABLED]: XmtpDisabledPayload; - [XMTP_ENABLED]: XmtpEnabledPayload; - [DENTITY_DISABLED]: DentityDisabledPayload; - [DENTITY_ENABLED]: DentityEnabledPayload; +export interface PluginsEventPayload { + [PLUGIN_TOGGLED]: PluginToggledPayload; + [VERIFICATION_TOGGLED]: VerificationToggledPayload; } diff --git a/apps/console/src/analytics/events/plugins/just-verified-disabled.ts b/apps/console/src/analytics/events/plugins/just-verified-disabled.ts deleted file mode 100644 index 04e21599..00000000 --- a/apps/console/src/analytics/events/plugins/just-verified-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const JUST_VERIFIED_DISABLED = 'JUST_VERIFIED_DISABLED'; - -export interface JustVerifiedDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/just-verified-enabled.ts b/apps/console/src/analytics/events/plugins/just-verified-enabled.ts deleted file mode 100644 index 638c66c7..00000000 --- a/apps/console/src/analytics/events/plugins/just-verified-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const JUST_VERIFIED_ENABLED = 'JUST_VERIFIED_ENABLED'; - -export interface JustVerifiedEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/discord-disabled.ts b/apps/console/src/analytics/events/plugins/justVerified/discord-disabled.ts deleted file mode 100644 index daaac7d4..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/discord-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const DISCORD_DISABLED = 'DISCORD_DISABLED'; - -export interface DiscordDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/discord-enabled.ts b/apps/console/src/analytics/events/plugins/justVerified/discord-enabled.ts deleted file mode 100644 index 87d874ff..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/discord-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const DISCORD_ENABLED = 'DISCORD_ENABLED'; - -export interface DiscordEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/email-disabled.ts b/apps/console/src/analytics/events/plugins/justVerified/email-disabled.ts deleted file mode 100644 index bce005b9..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/email-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const EMAIL_DISABLED = 'EMAIL_DISABLED'; - -export interface EmailDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/email-enabled.ts b/apps/console/src/analytics/events/plugins/justVerified/email-enabled.ts deleted file mode 100644 index f1ca9986..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/email-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const EMAIL_ENABLED = 'EMAIL_ENABLED'; - -export interface EmailEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/github-disabled.ts b/apps/console/src/analytics/events/plugins/justVerified/github-disabled.ts deleted file mode 100644 index 279b1c9c..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/github-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const GITHUB_DISABLED = 'GITHUB_DISABLED'; - -export interface GithubDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/github-enabled.ts b/apps/console/src/analytics/events/plugins/justVerified/github-enabled.ts deleted file mode 100644 index f2238060..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/github-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const GITHUB_ENABLED = 'GITHUB_ENABLED'; - -export interface GithubEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/index.ts b/apps/console/src/analytics/events/plugins/justVerified/index.ts deleted file mode 100644 index 9b9aea16..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { DISCORD_DISABLED, DiscordDisabledPayload } from './discord-disabled'; -import { DISCORD_ENABLED, DiscordEnabledPayload } from './discord-enabled'; -import { EMAIL_DISABLED, EmailDisabledPayload } from './email-disabled'; -import { EMAIL_ENABLED, EmailEnabledPayload } from './email-enabled'; -import { GITHUB_DISABLED, GithubDisabledPayload } from './github-disabled'; -import { GITHUB_ENABLED, GithubEnabledPayload } from './github-enabled'; -import { - TELEGRAM_DISABLED, - TelegramDisabledPayload, -} from './telegram-disabled'; -import { TELEGRAM_ENABLED, TelegramEnabledPayload } from './telegram-enabled'; -import { TWITTER_DISABLED, TwitterDisabledPayload } from './twitter-disabled'; -import { TWITTER_ENABLED, TwitterEnabledPayload } from './twitter-enabled'; - -export const JUST_VERIFIED_EVENTS = { - DISCORD_DISABLED, - DISCORD_ENABLED, - EMAIL_DISABLED, - EMAIL_ENABLED, - GITHUB_DISABLED, - GITHUB_ENABLED, - TELEGRAM_DISABLED, - TELEGRAM_ENABLED, - TWITTER_DISABLED, - TWITTER_ENABLED, -} as const; - -export interface JustVerifiedEventsPayload { - [DISCORD_DISABLED]: DiscordDisabledPayload; - [DISCORD_ENABLED]: DiscordEnabledPayload; - [EMAIL_DISABLED]: EmailDisabledPayload; - [EMAIL_ENABLED]: EmailEnabledPayload; - [GITHUB_DISABLED]: GithubDisabledPayload; - [GITHUB_ENABLED]: GithubEnabledPayload; - [TELEGRAM_DISABLED]: TelegramDisabledPayload; - [TELEGRAM_ENABLED]: TelegramEnabledPayload; - [TWITTER_DISABLED]: TwitterDisabledPayload; - [TWITTER_ENABLED]: TwitterEnabledPayload; -} diff --git a/apps/console/src/analytics/events/plugins/justVerified/telegram-disabled.ts b/apps/console/src/analytics/events/plugins/justVerified/telegram-disabled.ts deleted file mode 100644 index 43030e15..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/telegram-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const TELEGRAM_DISABLED = 'TELEGRAM_DISABLED'; - -export interface TelegramDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/telegram-enabled.ts b/apps/console/src/analytics/events/plugins/justVerified/telegram-enabled.ts deleted file mode 100644 index 064870ac..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/telegram-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const TELEGRAM_ENABLED = 'TELEGRAM_ENABLED'; - -export interface TelegramEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/twitter-disabled.ts b/apps/console/src/analytics/events/plugins/justVerified/twitter-disabled.ts deleted file mode 100644 index 340e4970..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/twitter-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const TWITTER_DISABLED = 'TWITTER_DISABLED'; - -export interface TwitterDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/justVerified/twitter-enabled.ts b/apps/console/src/analytics/events/plugins/justVerified/twitter-enabled.ts deleted file mode 100644 index 7afd8f7a..00000000 --- a/apps/console/src/analytics/events/plugins/justVerified/twitter-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const TWITTER_ENABLED = 'TWITTER_ENABLED'; - -export interface TwitterEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/plugin-toggled.ts b/apps/console/src/analytics/events/plugins/plugin-toggled.ts new file mode 100644 index 00000000..2daacf7b --- /dev/null +++ b/apps/console/src/analytics/events/plugins/plugin-toggled.ts @@ -0,0 +1,13 @@ +export const PLUGIN_TOGGLED = 'PLUGIN_TOGGLED'; + +export type PluginName = + | 'efp' + | 'poap' + | 'xmtp' + | 'dentity' + | 'just_verified'; + +export interface PluginToggledPayload { + plugin: PluginName; + enabled: boolean; +} diff --git a/apps/console/src/analytics/events/plugins/poap-disabled.ts b/apps/console/src/analytics/events/plugins/poap-disabled.ts deleted file mode 100644 index aeec0b0f..00000000 --- a/apps/console/src/analytics/events/plugins/poap-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const POAP_DISABLED = 'POAP_DISABLED'; - -export interface PoapDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/poap-enabled.ts b/apps/console/src/analytics/events/plugins/poap-enabled.ts deleted file mode 100644 index 676920bf..00000000 --- a/apps/console/src/analytics/events/plugins/poap-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const POAP_ENABLED = 'POAP_ENABLED'; - -export interface PoapEnabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/verification-toggled.ts b/apps/console/src/analytics/events/plugins/verification-toggled.ts new file mode 100644 index 00000000..392db143 --- /dev/null +++ b/apps/console/src/analytics/events/plugins/verification-toggled.ts @@ -0,0 +1,13 @@ +export const VERIFICATION_TOGGLED = 'VERIFICATION_TOGGLED'; + +export type VerificationProvider = + | 'twitter' + | 'telegram' + | 'github' + | 'discord' + | 'email'; + +export interface VerificationToggledPayload { + provider: VerificationProvider; + enabled: boolean; +} diff --git a/apps/console/src/analytics/events/plugins/xmtp-disabled.ts b/apps/console/src/analytics/events/plugins/xmtp-disabled.ts deleted file mode 100644 index c806cf9e..00000000 --- a/apps/console/src/analytics/events/plugins/xmtp-disabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const XMTP_DISABLED = 'XMTP_DISABLED'; - -export interface XmtpDisabledPayload {} diff --git a/apps/console/src/analytics/events/plugins/xmtp-enabled.ts b/apps/console/src/analytics/events/plugins/xmtp-enabled.ts deleted file mode 100644 index a6070183..00000000 --- a/apps/console/src/analytics/events/plugins/xmtp-enabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const XMTP_ENABLED = 'XMTP_ENABLED'; - -export interface XmtpEnabledPayload {} diff --git a/apps/console/src/analytics/events/signSection/any-ens-selected.ts b/apps/console/src/analytics/events/signSection/any-ens-selected.ts deleted file mode 100644 index b0b49643..00000000 --- a/apps/console/src/analytics/events/signSection/any-ens-selected.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const ANY_ENS_SELECTED = 'ANY_ENS_SELECTED'; - -export interface AnyEnsSelectedPayload {} diff --git a/apps/console/src/analytics/events/signSection/claimable-ens-selected.ts b/apps/console/src/analytics/events/signSection/claimable-ens-selected.ts deleted file mode 100644 index ed6b503a..00000000 --- a/apps/console/src/analytics/events/signSection/claimable-ens-selected.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const CLAIMABLE_ENS_SELECTED = 'CLAIMABLE_ENS_SELECTED'; - -export interface ClaimableEnsSelectedPayload {} diff --git a/apps/console/src/analytics/events/signSection/ens-selection-changed.ts b/apps/console/src/analytics/events/signSection/ens-selection-changed.ts new file mode 100644 index 00000000..110eb266 --- /dev/null +++ b/apps/console/src/analytics/events/signSection/ens-selection-changed.ts @@ -0,0 +1,10 @@ +export const ENS_SELECTION_CHANGED = 'ENS_SELECTION_CHANGED'; + +export type EnsSelectionMode = 'any' | 'claimable' | 'specific'; + +export interface EnsSelectionChangedPayload { + /** Which sign-in allow-list mode the user picked. */ + mode: EnsSelectionMode; + /** The specific ENS entered, only present when mode === 'specific'. */ + ens?: string; +} diff --git a/apps/console/src/analytics/events/signSection/index.ts b/apps/console/src/analytics/events/signSection/index.ts index 9af93a0d..d0715189 100644 --- a/apps/console/src/analytics/events/signSection/index.ts +++ b/apps/console/src/analytics/events/signSection/index.ts @@ -1,21 +1,12 @@ -import { ANY_ENS_SELECTED, AnyEnsSelectedPayload } from './any-ens-selected'; import { - CLAIMABLE_ENS_SELECTED, - ClaimableEnsSelectedPayload, -} from './claimable-ens-selected'; -import { - SPECIFIC_ENS_SELECTED, - SpecificEnsSelectedPayload, -} from './specific-ens-selected'; + ENS_SELECTION_CHANGED, + EnsSelectionChangedPayload, +} from './ens-selection-changed'; export const SIGN_SECTION_EVENTS = { - ANY_ENS_SELECTED, - SPECIFIC_ENS_SELECTED, - CLAIMABLE_ENS_SELECTED, + ENS_SELECTION_CHANGED, } as const; export interface SignSectionEventPayload { - [ANY_ENS_SELECTED]: AnyEnsSelectedPayload; - [SPECIFIC_ENS_SELECTED]: SpecificEnsSelectedPayload; - [CLAIMABLE_ENS_SELECTED]: ClaimableEnsSelectedPayload; + [ENS_SELECTION_CHANGED]: EnsSelectionChangedPayload; } diff --git a/apps/console/src/analytics/events/signSection/specific-ens-selected.ts b/apps/console/src/analytics/events/signSection/specific-ens-selected.ts deleted file mode 100644 index 21fcacda..00000000 --- a/apps/console/src/analytics/events/signSection/specific-ens-selected.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const SPECIFIC_ENS_SELECTED = 'SPECIFIC_ENS_SELECTED'; - -export interface SpecificEnsSelectedPayload { - ens: string; -} diff --git a/apps/console/src/analytics/index.ts b/apps/console/src/analytics/index.ts index ed7f119e..fca36ef8 100644 --- a/apps/console/src/analytics/index.ts +++ b/apps/console/src/analytics/index.ts @@ -20,20 +20,33 @@ class Index { } posthog.init(key, { api_host: host, + // With a reverse proxy api_host, posthog-js can't infer the app URL, so + // toolbar / session-replay / "view in PostHog" links break without this. + ui_host: 'https://eu.posthog.com', capture_pageview: true, capture_pageleave: true, autocapture: false, + // Only create person profiles once we identify() a wallet; anonymous + // demo traffic is still captured but doesn't spawn empty profiles. + person_profiles: 'identified_only', loaded: (posthog) => { if (process.env.NODE_ENV === 'development') posthog.debug(); }, }); + // Super properties: attached to EVERY event so prod/preview traffic and + // the source app are always sliceable in PostHog. + posthog.register({ + app: 'console', + environment: process.env.NODE_ENV, + }); } } identify(id: string) { if (!analyticsEnabled) return; + // No alias() here: aliasing a distinct_id to itself is a no-op at best and + // can corrupt identity merges. identify() alone links anon -> wallet. posthog.identify(id); - posthog.alias(id); this.register({ id }); } diff --git a/apps/console/src/app/page.tsx b/apps/console/src/app/page.tsx index 0b7ff44f..e77e8c60 100644 --- a/apps/console/src/app/page.tsx +++ b/apps/console/src/app/page.tsx @@ -18,6 +18,7 @@ export default function Page() { const handleEnsClick = (ens: string) => { getAnalyticsClient().track('PROFILE_VIEWED', { ens, + location: 'demo_card', }); }; diff --git a/apps/console/src/components/sections/code/CodeSection/index.tsx b/apps/console/src/components/sections/code/CodeSection/index.tsx index 4ca36802..bc19825e 100644 --- a/apps/console/src/components/sections/code/CodeSection/index.tsx +++ b/apps/console/src/components/sections/code/CodeSection/index.tsx @@ -200,11 +200,18 @@ export default App;`.trim(); ]); const handleDependenciesCopy = () => { + getAnalyticsClient().track('CODE_COPIED', { + location: 'code_section', + snippet: 'dependencies', + }); navigator.clipboard.writeText(dependencies); }; const handleCopy = () => { - getAnalyticsClient().track('CODE_COPIED', {}); + getAnalyticsClient().track('CODE_COPIED', { + location: 'code_section', + snippet: 'integration', + }); navigator.clipboard.writeText(code); }; diff --git a/apps/console/src/components/sections/customizer/ClaimSection/index.tsx b/apps/console/src/components/sections/customizer/ClaimSection/index.tsx index a13b2412..40f20cfe 100644 --- a/apps/console/src/components/sections/customizer/ClaimSection/index.tsx +++ b/apps/console/src/components/sections/customizer/ClaimSection/index.tsx @@ -91,8 +91,12 @@ export const ClaimSection = () => { } ) .then((res) => { - getAnalyticsClient().track('ENS_BY_API_KEY_CALLED', {}); - setEnsByApiKey(res.data.result.data.domains); + const domains = res.data.result.data.domains; + getAnalyticsClient().track('ENS_BY_API_KEY_CALLED', { + location: 'claim_section', + domainCount: domains?.length ?? 0, + }); + setEnsByApiKey(domains); }) .catch((err) => { setEnsByApiKey([]); @@ -179,7 +183,10 @@ export const ClaimSection = () => { target={'_blank'} className={'text-primary'} onClick={() => { - getAnalyticsClient().track('DASHBOARD_LINK_CLICKED', {}); + getAnalyticsClient().track('LINK_CLICKED', { + target: 'dashboard', + location: 'claim_section', + }); }} > Dashboard diff --git a/apps/console/src/components/sections/customizer/Customizer/index.tsx b/apps/console/src/components/sections/customizer/Customizer/index.tsx index 8cf20e65..20c8bac4 100644 --- a/apps/console/src/components/sections/customizer/Customizer/index.tsx +++ b/apps/console/src/components/sections/customizer/Customizer/index.tsx @@ -49,6 +49,7 @@ export const Customizer = ({ mobile }: CustomizerProps) => { onCheckedChange={() => { getAnalyticsClient().track('NETWORK_CHANGED', { chainId: chainId === 1 ? 11155111 : 1, + network: chainId === 1 ? 'sepolia' : 'mainnet', }); switchChainAsync({ chainId: chainId === 1 ? 11155111 : 1, diff --git a/apps/console/src/components/sections/customizer/PluginsSection/Dentity/index.tsx b/apps/console/src/components/sections/customizer/PluginsSection/Dentity/index.tsx index 471090f3..a8b01b0b 100644 --- a/apps/console/src/components/sections/customizer/PluginsSection/Dentity/index.tsx +++ b/apps/console/src/components/sections/customizer/PluginsSection/Dentity/index.tsx @@ -18,7 +18,10 @@ export const Dentity = () => { DentityPlugin, ], }); - getAnalyticsClient().track('DENTITY_ENABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'dentity', + enabled: true, + }); } else { handleJustWeb3Config({ ...config, @@ -26,7 +29,10 @@ export const Dentity = () => { (plugin) => plugin.name !== DentityPlugin.name ), }); - getAnalyticsClient().track('DENTITY_DISABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'dentity', + enabled: false, + }); } }; diff --git a/apps/console/src/components/sections/customizer/PluginsSection/EFP/index.tsx b/apps/console/src/components/sections/customizer/PluginsSection/EFP/index.tsx index 7f608bb9..26ceced3 100644 --- a/apps/console/src/components/sections/customizer/PluginsSection/EFP/index.tsx +++ b/apps/console/src/components/sections/customizer/PluginsSection/EFP/index.tsx @@ -19,7 +19,10 @@ export const EFP = () => { EFPPlugin, ], }); - getAnalyticsClient().track('EFP_ENABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'efp', + enabled: true, + }); } else { handleJustWeb3Config({ ...config, @@ -27,7 +30,10 @@ export const EFP = () => { (plugin) => plugin.name !== EFPPlugin.name ), }); - getAnalyticsClient().track('EFP_DISABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'efp', + enabled: false, + }); } }; diff --git a/apps/console/src/components/sections/customizer/PluginsSection/JustVerified/index.tsx b/apps/console/src/components/sections/customizer/PluginsSection/JustVerified/index.tsx index 6d37d1ca..5f905d6a 100644 --- a/apps/console/src/components/sections/customizer/PluginsSection/JustVerified/index.tsx +++ b/apps/console/src/components/sections/customizer/PluginsSection/JustVerified/index.tsx @@ -67,7 +67,10 @@ export const JustVerified = () => { ), ], }); - getAnalyticsClient().track('JUST_VERIFIED_ENABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'just_verified', + enabled: true, + }); } else { handleJustWeb3Config({ ...config, @@ -75,31 +78,22 @@ export const JustVerified = () => { (plugin) => plugin.name !== 'JustVerifiedPlugin' ), }); - getAnalyticsClient().track('JUST_VERIFIED_DISABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'just_verified', + enabled: false, + }); } }; const handleSocialEnabledAnalytics = (credential: Credentials, unCheck: boolean) => { - switch (credential) { - case 'twitter': - getAnalyticsClient().track(unCheck ? 'TWITTER_DISABLED' : 'TWITTER_ENABLED', {}); - break; - case 'telegram': - getAnalyticsClient().track(unCheck ? 'TELEGRAM_DISABLED' : 'TELEGRAM_ENABLED', {}); - break; - case 'github': - getAnalyticsClient().track(unCheck ? 'GITHUB_DISABLED' : 'GITHUB_ENABLED', {}); - break; - case 'discord': - getAnalyticsClient().track(unCheck ? 'DISCORD_DISABLED' : 'DISCORD_ENABLED', {}); - break; - case 'email': - getAnalyticsClient().track(unCheck ? 'EMAIL_DISABLED' : 'EMAIL_ENABLED', {}); - break; - default: - break; - } + // `Credentials` ('twitter' | 'telegram' | ...) maps 1:1 onto our + // VerificationProvider union, so a single typed event replaces the old + // 10-event switch. + getAnalyticsClient().track('VERIFICATION_TOGGLED', { + provider: credential, + enabled: !unCheck, + }); showToast('success', "Code Updated!", `justverified-social-${credential}`) }; diff --git a/apps/console/src/components/sections/customizer/PluginsSection/POAP/index.tsx b/apps/console/src/components/sections/customizer/PluginsSection/POAP/index.tsx index e3c92f19..d5b640b7 100644 --- a/apps/console/src/components/sections/customizer/PluginsSection/POAP/index.tsx +++ b/apps/console/src/components/sections/customizer/PluginsSection/POAP/index.tsx @@ -19,7 +19,10 @@ export const POAP = () => { POAPPluginInstance, ], }); - getAnalyticsClient().track('POAP_ENABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'poap', + enabled: true, + }); } else { handleJustWeb3Config({ ...config, @@ -27,7 +30,10 @@ export const POAP = () => { (plugin) => plugin.name !== POAPPluginInstance.name ), }); - getAnalyticsClient().track('POAP_DISABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'poap', + enabled: false, + }); } }; diff --git a/apps/console/src/components/sections/customizer/PluginsSection/XMTP/index.tsx b/apps/console/src/components/sections/customizer/PluginsSection/XMTP/index.tsx index 15f801c9..c471f0df 100644 --- a/apps/console/src/components/sections/customizer/PluginsSection/XMTP/index.tsx +++ b/apps/console/src/components/sections/customizer/PluginsSection/XMTP/index.tsx @@ -18,7 +18,10 @@ export const XMTP = () => { XMTPPlugin('production'), ], }); - getAnalyticsClient().track('XMTP_ENABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'xmtp', + enabled: true, + }); } else { handleJustWeb3Config({ ...config, @@ -26,7 +29,10 @@ export const XMTP = () => { (plugin) => plugin.name !== 'XMTPPlugin' ), }); - getAnalyticsClient().track('XMTP_DISABLED', {}); + getAnalyticsClient().track('PLUGIN_TOGGLED', { + plugin: 'xmtp', + enabled: false, + }); }; } diff --git a/apps/console/src/components/sections/customizer/SignSection/index.tsx b/apps/console/src/components/sections/customizer/SignSection/index.tsx index b6b1fcd4..6874be23 100644 --- a/apps/console/src/components/sections/customizer/SignSection/index.tsx +++ b/apps/console/src/components/sections/customizer/SignSection/index.tsx @@ -31,10 +31,14 @@ export const SignSection = () => { const action = e as JustWeb3ProviderConfig['allowedEns']; switch (action) { case 'all': - getAnalyticsClient().track('ANY_ENS_SELECTED', {}); + getAnalyticsClient().track('ENS_SELECTION_CHANGED', { + mode: 'any', + }); break; case 'claimable': - getAnalyticsClient().track('CLAIMABLE_ENS_SELECTED', {}); + getAnalyticsClient().track('ENS_SELECTION_CHANGED', { + mode: 'claimable', + }); break; } handleJustWeb3Config({ @@ -62,7 +66,8 @@ export const SignSection = () => { placeholder="Add ENS" onKeyUp={(e) => { if (e.key === 'Enter') { - getAnalyticsClient().track('SPECIFIC_ENS_SELECTED', { + getAnalyticsClient().track('ENS_SELECTION_CHANGED', { + mode: 'specific', ens: ensInput, }); setEnsList([...ensList, ensInput]); diff --git a/apps/console/src/layout/navbar/index.tsx b/apps/console/src/layout/navbar/index.tsx index 65c66c1a..4b6fdd59 100644 --- a/apps/console/src/layout/navbar/index.tsx +++ b/apps/console/src/layout/navbar/index.tsx @@ -13,7 +13,12 @@ export const Navbar = () => { href={'https://docs.justaname.id'} passHref target="_blank" - onClick={() => getAnalyticsClient().track('DOCS_LINK_CLICKED', {})} + onClick={() => + getAnalyticsClient().track('LINK_CLICKED', { + target: 'docs', + location: 'navbar', + }) + } > From 2498d6396c9ea0b9d3484ca73fee0a3b90c29810 Mon Sep 17 00:00:00 2001 From: Anthony Khoury Date: Fri, 3 Jul 2026 14:39:35 +0300 Subject: [PATCH 3/3] feat: replaced siwe to complete remove ethers (#149) --- .github/workflows/release.yml | 18 +- .gitignore | 3 + docs/guide/wallet-providers/para.md | 6 +- docs/sdk/JustaName Core SDK/README.md | 10 +- docs/sdk/siwens/README.md | 6 +- package.json | 1 - packages/@justaname.id/sdk/README.md | 12 +- packages/@justaname.id/sdk/package.json | 1 - .../sdk/src/lib/features/sign-in/index.ts | 65 +--- .../lib/features/subname-challenge/index.ts | 15 +- packages/@justaname.id/siwens/README.md | 7 +- packages/@justaname.id/siwens/package.json | 2 +- .../@justaname.id/siwens/src/lib/index.ts | 1 + .../siwens/src/lib/siwens/siwens.ts | 285 ++++++++++++++---- .../siwens/src/lib/types/index.ts | 88 ++++++ .../src/lib/utils/generateNonce/index.ts | 19 ++ .../siwens/src/lib/utils/index.ts | 3 +- .../siwens/src/test/siwens.format.spec.ts | 119 ++++++++ yarn.lock | 50 +-- 19 files changed, 519 insertions(+), 192 deletions(-) create mode 100644 packages/@justaname.id/siwens/src/lib/types/index.ts create mode 100644 packages/@justaname.id/siwens/src/lib/utils/generateNonce/index.ts create mode 100644 packages/@justaname.id/siwens/src/test/siwens.format.spec.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c719670..46c01a10 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,9 +70,25 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: rm -rf node_modules/.cache/rollup-plugin-typescript2 + - name: Determine version specifier from commit title + id: specifier + env: + COMMIT_MSG: ${{ github.event.head_commit.message }} + run: | + TITLE=$(printf '%s' "$COMMIT_MSG" | head -n1) + echo "Commit title: $TITLE" + if printf '%s' "$TITLE" | grep -qE '^feat(\(.+\))?!?:'; then + SPECIFIER=minor + elif printf '%s' "$TITLE" | grep -qE '^fix(\(.+\))?!?:'; then + SPECIFIER=patch + else + SPECIFIER=patch + fi + echo "Resolved specifier: $SPECIFIER" + echo "specifier=$SPECIFIER" >> "$GITHUB_OUTPUT" - name: Release run: | - npx nx release --specifier=patch --yes + npx nx release --specifier=${{ steps.specifier.outputs.specifier }} --yes env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index e5c49852..da64f306 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ node_modules !.vscode/extensions.json .cursor +/.env +/.env.local + # misc /.sass-cache /connect.lock diff --git a/docs/guide/wallet-providers/para.md b/docs/guide/wallet-providers/para.md index f7a4f66c..f5e65160 100644 --- a/docs/guide/wallet-providers/para.md +++ b/docs/guide/wallet-providers/para.md @@ -8,19 +8,19 @@ Run the following command to install the necessary packages: {% tabs %} {% tab title="npm" %} -
npm install @justweb3/widget @getpara/react-sdk @getpara/wagmi-v2-integration wagmi @tanstack/react-query ethers
+
npm install @justweb3/widget @getpara/react-sdk @getpara/wagmi-v2-integration wagmi @tanstack/react-query
 
{% endtab %} {% tab title="pnpm" %} ```bash -pnpm install @justweb3/widget @getpara/react-sdk @getpara/wagmi-v2-integration wagmi @tanstack/react-query ethers +pnpm install @justweb3/widget @getpara/react-sdk @getpara/wagmi-v2-integration wagmi @tanstack/react-query ``` {% endtab %} {% tab title="yarn" %} ```bash -yarn add @justweb3/widget @getpara/react-sdk @getpara/wagmi-v2-integration wagmi @tanstack/react-query ethers +yarn add @justweb3/widget @getpara/react-sdk @getpara/wagmi-v2-integration wagmi @tanstack/react-query ``` {% endtab %} {% endtabs %} diff --git a/docs/sdk/JustaName Core SDK/README.md b/docs/sdk/JustaName Core SDK/README.md index fc98e0d4..4fb8e676 100644 --- a/docs/sdk/JustaName Core SDK/README.md +++ b/docs/sdk/JustaName Core SDK/README.md @@ -60,7 +60,7 @@ First, import the JustaName SDK and initialize it with your configuration: ```typescript import { JustaName } from '@justaname.id/sdk'; -import { ethers } from 'ethers'; +import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'; // Initialize the SDK with your configuration const justaname = JustaName.init({ @@ -85,7 +85,7 @@ const justaname = JustaName.init({ }); // Create a signer (for example purposes, we're creating a random wallet) -const signer = ethers.Wallet.createRandom(); +const signer = privateKeyToAccount(generatePrivateKey()); ``` ### Issuing a Subname @@ -98,7 +98,7 @@ async function issueSubname() { chainId: 1 // Ethereum Mainnet }); - const signature = await signer.signMessage(challenge.challenge); + const signature = await signer.signMessage({ message: challenge.challenge }); const response = await justaname.subnames.addSubname( { @@ -125,7 +125,7 @@ async function updateSubname() { chainId: 1 }); - const signature = await signer.signMessage(challenge.challenge); + const signature = await signer.signMessage({ message: challenge.challenge }); const response = await justaname.subnames.updateSubname( { @@ -159,7 +159,7 @@ async function signIn() { address: signer.address }); - const signature = await signer.signMessage(message); + const signature = await signer.signMessage({ message }); const response = await justaname.signIn.signIn({ message: message, diff --git a/docs/sdk/siwens/README.md b/docs/sdk/siwens/README.md index e912f1a9..de9e47b9 100644 --- a/docs/sdk/siwens/README.md +++ b/docs/sdk/siwens/README.md @@ -48,12 +48,12 @@ yarn add @justaname.id/siwens ### Example Usage ```typescript import { SIWENS, InvalidDomainException, InvalidENSException, InvalidStatementException, InvalidTimeException } f, InvalidDomainException, InvalidENSException, InvalidStatementException, InvalidTimeException } from '@justaname.id/siwens';rom '@justaname.id/siwens'; -import { ethers } from 'ethers'; +import { privateKeyToAccount } from 'viem/accounts'; // Define your provider URL (e.g., Infura) const providerUrl = 'https://mainnet.infura.io/v3/YOUR_INFURA_KEY'; -const signer = new ethers.Wallet('YOUR_PRIVATE_KEY_ENS_HOLDER') +const signer = privateKeyToAccount('YOUR_PRIVATE_KEY_ENS_HOLDER') async function signInUser() { const siwens = new SIWENS({ @@ -66,7 +66,7 @@ async function signInUser() { providerUrl }); const message = await siwens.prepareMessage(); - const signature = await signer.signMessage(message); + const signature = await signer.signMessage({ message }); return signature; } diff --git a/package.json b/package.json index d488bb73..1a7840c7 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,6 @@ "react-router-dom": "6.11.2", "react-timer-hook": "3.0.8", "react-tiny-popover": "8.0.4", - "siwe": "2.3.2", "tailwind-merge": "2.5.2", "tailwindcss-animate": "1.0.7", "tslib": "2.3.0", diff --git a/packages/@justaname.id/sdk/README.md b/packages/@justaname.id/sdk/README.md index 136f2e2d..3759edef 100644 --- a/packages/@justaname.id/sdk/README.md +++ b/packages/@justaname.id/sdk/README.md @@ -56,7 +56,7 @@ First, import the JustaName SDK and initialize it with your configuration: ```typescript import { JustaName } from '@justaname.id/sdk'; -import { ethers } from 'ethers'; +import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'; // Initialize the SDK with your configuration const justaname = JustaName.init({ @@ -80,8 +80,8 @@ const justaname = JustaName.init({ } }); -// Create a signer (for example purposes, we're creating a random wallet) -const signer = ethers.Wallet.createRandom(); +// Create a signer (for example purposes, we're creating a random account) +const signer = privateKeyToAccount(generatePrivateKey()); ``` ### Issuing a Subname @@ -94,7 +94,7 @@ async function issueSubname() { chainId: 1 // Ethereum Mainnet }); - const signature = await signer.signMessage(challenge.challenge); + const signature = await signer.signMessage({ message: challenge.challenge }); const response = await justaname.subnames.addSubname( { @@ -121,7 +121,7 @@ async function updateSubname() { chainId: 1 }); - const signature = await signer.signMessage(challenge.challenge); + const signature = await signer.signMessage({ message: challenge.challenge }); const response = await justaname.subnames.updateSubname( { @@ -155,7 +155,7 @@ async function signIn() { address: signer.address }); - const signature = await signer.signMessage(message); + const signature = await signer.signMessage({ message }); const response = await justaname.signIn.signIn({ message: message, diff --git a/packages/@justaname.id/sdk/package.json b/packages/@justaname.id/sdk/package.json index 721d209b..6a08ede3 100644 --- a/packages/@justaname.id/sdk/package.json +++ b/packages/@justaname.id/sdk/package.json @@ -10,7 +10,6 @@ "jest": "^29.4.1" }, "peerDependencies": { - "siwe": ">=2.0.0", "viem": "^2.48.0" }, "exports": { diff --git a/packages/@justaname.id/sdk/src/lib/features/sign-in/index.ts b/packages/@justaname.id/sdk/src/lib/features/sign-in/index.ts index c7619201..7f7ce548 100644 --- a/packages/@justaname.id/sdk/src/lib/features/sign-in/index.ts +++ b/packages/@justaname.id/sdk/src/lib/features/sign-in/index.ts @@ -8,8 +8,6 @@ import { } from '../../errors'; import { OffchainResolvers } from '../offchain-resolvers'; import { RequestSignInParams, SignInFunctionParams } from '../../types/signin'; -import { createPublicClient, http } from 'viem'; -import { mainnet, sepolia } from 'viem/chains'; import { normalize } from 'viem/ens'; export interface SignInResponse extends SiwensResponse { @@ -110,62 +108,13 @@ export class SignIn { providerUrl: network.providerUrl, }); - const siwensResponse = await siwens.verify( - { - signature: params.signature, - nonce: params.nonce, - domain: params.domain, - }, - { - // Smart-contract (EIP-1271) verification is handled inside - // `verificationFallback` below using viem's `verifySiweMessage`. - // We no longer pass `provider` here because it must be an ethers - // `Provider`, and the SDK is now viem-only. - verificationFallback: async (params, opts, message, EIP1271Promise) => { - // Use the chainId extracted from the SIWE message itself, not the - // SDK-default. Otherwise contract-wallet (EIP-1271) verification - // runs against the wrong chain when the message is cross-chain. - const publicClient = createPublicClient({ - chain: chainId === 1 ? mainnet : sepolia, - transport: http(network.providerUrl), - }); - - const result = await EIP1271Promise; - - if (result.success) { - return result; - } else { - let signature = params.signature; - const lastByte = parseInt(params.signature.slice(-2), 16); - if (lastByte < 27) { - const adjustedV = (27 + (lastByte % 2)) - .toString(16) - .padStart(2, '0'); - signature = signature.slice(0, -2) + adjustedV; - } - - const viemResponse = await publicClient.verifySiweMessage({ - message: message.toMessage(), - signature: signature as `0x${string}`, - address: result.data.address as `0x${string}`, - nonce: params.nonce, - domain: params.domain as string, - time: params.time ? new Date(params.time) : undefined, - scheme: params.scheme as string, - }); - - if (viemResponse) { - return { - data: result.data, - success: true, - }; - } - - return result; - } - }, - } - ); + // SIWENS.verify performs EOA recovery, EIP-1271 and ERC-6492 verification + // internally via viem (`verifySiweMessage`) against the message's chain. + const siwensResponse = await siwens.verify({ + signature: params.signature, + nonce: params.nonce, + domain: params.domain, + }); if (siwensResponse.data.chainId !== chainId) { throw InvalidSignInException.chainIdMismatch( diff --git a/packages/@justaname.id/sdk/src/lib/features/subname-challenge/index.ts b/packages/@justaname.id/sdk/src/lib/features/subname-challenge/index.ts index 5d065d22..e8fea9e5 100644 --- a/packages/@justaname.id/sdk/src/lib/features/subname-challenge/index.ts +++ b/packages/@justaname.id/sdk/src/lib/features/subname-challenge/index.ts @@ -6,7 +6,9 @@ import { } from '../../types'; import { SiweConfig } from '../../types/siwe/siwe-config'; import { ChallengeRequestException } from '../../errors/ChallengeRequest.expection'; -import { SiweMessage } from 'siwe'; +import { generateNonce } from '@justaname.id/siwens'; +import { getAddress } from 'viem'; +import { createSiweMessage } from 'viem/siwe'; /** * Represents the Sign-In with Ethereum (SIWE) functionality, providing methods @@ -104,19 +106,18 @@ export class SubnameChallenge { const { expirationTime, issuedAt } = this.generateIssuedAndExpirationTime(_ttl); - const siweMessage = new SiweMessage({ + const prepared = createSiweMessage({ domain: _domain, uri: _origin, - address: _address, + address: getAddress(_address), statement: statement, chainId: _chainId, version: '1', - issuedAt, - expirationTime, + nonce: generateNonce(), + issuedAt: new Date(issuedAt), + expirationTime: new Date(expirationTime), }); - const prepared = siweMessage.prepareMessage(); - if (this.dev) { // eslint-disable-next-line no-console console.debug( diff --git a/packages/@justaname.id/siwens/README.md b/packages/@justaname.id/siwens/README.md index 727d6e1b..3fc11f3c 100644 --- a/packages/@justaname.id/siwens/README.md +++ b/packages/@justaname.id/siwens/README.md @@ -44,14 +44,13 @@ yarn add @justaname.id/siwens ### Example Usage ```typescript import { SIWENS, InvalidENSException } from '@justaname.id/siwens'; -import { Wallet } from 'ethers'; +import { privateKeyToAccount } from 'viem/accounts'; // Define your provider URL (e.g., Infura) const infuraProjectId = 'YOUR_INFURA_PROJECT_ID'; const providerUrl = 'https://mainnet.infura.io/v3/' + infuraProjectId; -// const signer = Wallet.createRandom(); -const signer = new Wallet('YOUR_PRIVATE_KEY'); +const signer = privateKeyToAccount('YOUR_PRIVATE_KEY'); async function signInUser() { const siwens = new SIWENS({ @@ -67,7 +66,7 @@ async function signInUser() { providerUrl }); const message = await siwens.prepareMessage(); - const signature = await signer.signMessage(message); + const signature = await signer.signMessage({ message }); return {signature, message}; } diff --git a/packages/@justaname.id/siwens/package.json b/packages/@justaname.id/siwens/package.json index 1125c483..9cbd9d91 100644 --- a/packages/@justaname.id/siwens/package.json +++ b/packages/@justaname.id/siwens/package.json @@ -2,10 +2,10 @@ "name": "@justaname.id/siwens", "version": "0.0.145", "dependencies": { + "@stablelib/random": "^1.0.2", "punycode": "^2.3.1" }, "peerDependencies": { - "siwe": ">=2.0.0", "viem": "^2.48.0" }, "exports": { diff --git a/packages/@justaname.id/siwens/src/lib/index.ts b/packages/@justaname.id/siwens/src/lib/index.ts index 10ca5f1f..652b428b 100644 --- a/packages/@justaname.id/siwens/src/lib/index.ts +++ b/packages/@justaname.id/siwens/src/lib/index.ts @@ -1,3 +1,4 @@ export * from './errors'; +export * from './types'; export * from './siwens/siwens'; export * from './utils'; \ No newline at end of file diff --git a/packages/@justaname.id/siwens/src/lib/siwens/siwens.ts b/packages/@justaname.id/siwens/src/lib/siwens/siwens.ts index 735df385..e97ec5d3 100644 --- a/packages/@justaname.id/siwens/src/lib/siwens/siwens.ts +++ b/packages/@justaname.id/siwens/src/lib/siwens/siwens.ts @@ -1,10 +1,3 @@ -import { - generateNonce, - SiweMessage, - SiweResponse, - VerifyOpts, - VerifyParams, -} from 'siwe'; import { InvalidConfigurationException, InvalidENSException, @@ -16,7 +9,16 @@ import { checkTTL, constructSignInStatement, extractDataFromStatement, + generateNonce, } from '../utils'; +import { + SiweError, + SiweErrorType, + SiweMessageFields, + SiweResponse, + VerifyOpts, + VerifyParams, +} from '../types'; import { toASCII, toUnicode } from 'punycode'; import { createPublicClient, @@ -28,6 +30,11 @@ import { import { mainnet, sepolia } from 'viem/chains'; import type { Chain } from 'viem'; import { normalize } from 'viem/ens'; +import { + createSiweMessage, + parseSiweMessage, + verifySiweMessage, +} from 'viem/siwe'; const SUPPORTED_CHAINS: Record = { 1: mainnet, @@ -43,14 +50,18 @@ const buildPublicClient = ( transport: http(providerUrl), }); +const toISOStringOrUndefined = (value?: string | Date): string | undefined => { + if (!value) { + return undefined; + } + return value instanceof Date ? value.toISOString() : value; +}; + export interface SiwensResponse extends SiweResponse { ens: string; } -export interface SiwensParams - extends Partial< - Omit - > { +export interface SiwensParams extends Partial { ens: string; ttl?: number; expirationTime?: string; @@ -62,19 +73,57 @@ export interface SiwensConfig { providerUrl?: string; } -export class SIWENS extends SiweMessage { +/** + * Sign-In with ENS message. Previously this extended `siwe`'s `SiweMessage`; + * it is now a standalone, ethers-free implementation backed by viem's native + * SIWE module (`viem/siwe`). The public surface (fields, `prepareMessage`, + * `verify`, `generateNonce`) is preserved. + */ +export class SIWENS { + readonly scheme?: string; + readonly domain: string; + readonly address: string; + readonly statement?: string; + readonly uri: string; + readonly version: string; + readonly chainId: number; + readonly nonce: string; + readonly issuedAt?: string; + readonly expirationTime?: string; + readonly notBefore?: string; + readonly requestId?: string; + readonly resources?: string[]; readonly provider: PublicClient; readonly providerUrl: string | undefined; + /** The raw EIP-4361 message string (parsed input, or the built message). */ + private readonly message: string; constructor(signInConfig: SiwensConfig) { const { params, providerUrl } = signInConfig; + if (typeof params === 'string') { - super(params); if (!providerUrl) { throw InvalidConfigurationException.providerUrlRequired(); } - this.provider = buildPublicClient(providerUrl, this.chainId); + const parsed = parseSiweMessage(params); + this.scheme = parsed.scheme; + this.domain = parsed.domain as string; + // Normalize to EIP-55 checksum so `data.address` matches the casing that + // `siwe` always returned (it rejected non-checksummed addresses). + this.address = viemGetAddress(parsed.address as string); + this.statement = parsed.statement; + this.uri = parsed.uri as string; + this.version = (parsed.version as string) || '1'; + this.chainId = (parsed.chainId as number) ?? 1; + this.nonce = parsed.nonce as string; + this.issuedAt = toISOStringOrUndefined(parsed.issuedAt); + this.expirationTime = toISOStringOrUndefined(parsed.expirationTime); + this.notBefore = toISOStringOrUndefined(parsed.notBefore); + this.requestId = parsed.requestId; + this.resources = parsed.resources; + this.message = params; this.providerUrl = providerUrl; + this.provider = buildPublicClient(providerUrl, this.chainId); return; } @@ -86,18 +135,11 @@ export class SIWENS extends SiweMessage { throw InvalidConfigurationException.domainRequired(); } - let issuedAt = params.issuedAt; - let expirationTime = params.expirationTime; - - if (params.ttl) { - checkTTL(params.ttl); - const { - issuedAt: issuedAtGenerated, - expirationTime: expirationTimeGenerated, - } = SIWENS.generateIssuedAndExpirationTime(params.ttl); - issuedAt = issuedAt || issuedAtGenerated; - expirationTime = expirationTime || expirationTimeGenerated; - } + checkTTL(params.ttl); + const { + issuedAt: issuedAtGenerated, + expirationTime: expirationTimeGenerated, + } = SIWENS.generateIssuedAndExpirationTime(params.ttl); checkDomainValid(params.ens); @@ -106,46 +148,140 @@ export class SIWENS extends SiweMessage { params?.statement || '' ); - super({ - ...params, - statement, - version: params.version || '1', - issuedAt, - expirationTime, - }); + this.scheme = params.scheme; + this.domain = params.domain; + this.address = viemGetAddress(params.address as string); + this.statement = statement; + this.uri = params.uri as string; + this.version = params.version || '1'; + this.chainId = (params.chainId as number) ?? 1; + this.nonce = params.nonce || generateNonce(); + this.issuedAt = params.issuedAt || issuedAtGenerated; + this.expirationTime = params.expirationTime || expirationTimeGenerated; + this.notBefore = params.notBefore; + this.requestId = params.requestId; + this.resources = params.resources; this.providerUrl = providerUrl; this.provider = buildPublicClient(providerUrl, this.chainId); + this.message = this.buildMessage(); + } + + toMessage(): string { + return this.message; + } + + prepareMessage(): string { + return this.message; } - override async verify( + async verify( params: VerifyParams, opts?: VerifyOpts ): Promise { - let verification: SiweResponse; + const suppress = opts?.suppressExceptions ?? false; + const data = this.toFields(); - try { - const { signature, ...rest } = params; - const _tempParams = { - signature, - ...rest, - }; - const lastByte = parseInt(signature.slice(-2), 16); - if (lastByte < 27) { - const adjustedV = (27 + (lastByte % 2)).toString(16).padStart(2, '0'); - _tempParams['signature'] = signature.slice(0, -2) + adjustedV; + const computeEns = (): string | undefined => { + try { + return this.statement + ? toUnicode(extractDataFromStatement(this.statement).ens) + : undefined; + } catch { + return undefined; } + }; - verification = await super.verify(_tempParams, opts); - } catch (e) { - const statement = e.data.statement; - const { ens } = extractDataFromStatement(statement); - throw { - ...e, - ens: toUnicode(ens), + const fail = (error: SiweError): SiwensResponse => { + const result: SiwensResponse = { + success: false, + data, + error, + ens: computeEns() as string, }; + if (suppress) { + return result; + } + throw result; + }; + + // Normalize legacy `v` values (< 27) to canonical 27/28 before verifying. + let signature = params.signature; + const lastByte = parseInt(signature.slice(-2), 16); + if (lastByte < 27) { + const adjustedV = (27 + (lastByte % 2)).toString(16).padStart(2, '0'); + signature = signature.slice(0, -2) + adjustedV; } - const statement = verification.data.statement; + // Field validation — mirrors `siwe`'s order and error types so the thrown + // shape is unchanged for consumers. + if (params.scheme && params.scheme !== this.scheme) { + return fail( + new SiweError(SiweErrorType.SCHEME_MISMATCH, params.scheme, this.scheme) + ); + } + if (params.domain && params.domain !== this.domain) { + return fail( + new SiweError(SiweErrorType.DOMAIN_MISMATCH, params.domain, this.domain) + ); + } + if (params.nonce && params.nonce !== this.nonce) { + return fail( + new SiweError(SiweErrorType.NONCE_MISMATCH, params.nonce, this.nonce) + ); + } + + const checkTime = new Date(params.time || new Date()); + if (this.expirationTime) { + const expirationDate = new Date(this.expirationTime); + if (checkTime.getTime() >= expirationDate.getTime()) { + return fail( + new SiweError( + SiweErrorType.EXPIRED_MESSAGE, + `${checkTime.toISOString()} < ${expirationDate.toISOString()}`, + `${checkTime.toISOString()} >= ${expirationDate.toISOString()}` + ) + ); + } + } + if (this.notBefore) { + const notBefore = new Date(this.notBefore); + if (checkTime.getTime() < notBefore.getTime()) { + return fail( + new SiweError( + SiweErrorType.NOT_YET_VALID_MESSAGE, + `${checkTime.toISOString()} >= ${notBefore.toISOString()}`, + `${checkTime.toISOString()} < ${notBefore.toISOString()}` + ) + ); + } + } + + // Signature verification — EOA recovery + EIP-1271 + ERC-6492 in a single + // viem call against the configured public client. A genuine signature + // mismatch resolves to `false`; operational errors (RPC/transport failures) + // are intentionally left to propagate rather than be masked as an invalid + // signature, so contract-wallet checks on a flaky RPC surface a real error. + const valid = await verifySiweMessage(this.provider, { + message: this.message, + signature: signature as `0x${string}`, + address: this.address as `0x${string}`, + ...(params.domain ? { domain: params.domain } : {}), + ...(params.nonce ? { nonce: params.nonce } : {}), + ...(params.scheme ? { scheme: params.scheme } : {}), + time: checkTime, + }); + + if (!valid) { + return fail( + new SiweError( + SiweErrorType.INVALID_SIGNATURE, + undefined, + `Resolved address to be ${this.address}` + ) + ); + } + + const statement = this.statement; if (!statement) { throw InvalidStatementException.invalidStatement(); } @@ -154,7 +290,8 @@ export class SIWENS extends SiweMessage { await this.verifyEnsAddress(ens, this.address); return { - ...verification, + success: true, + data, ens, }; } @@ -169,10 +306,48 @@ export class SIWENS extends SiweMessage { }; } - static generateNonce() { + static generateNonce(): string { return generateNonce(); } + private toFields(): SiweMessageFields { + return { + scheme: this.scheme, + domain: this.domain, + address: this.address, + statement: this.statement, + uri: this.uri, + version: this.version, + chainId: this.chainId, + nonce: this.nonce, + issuedAt: this.issuedAt, + expirationTime: this.expirationTime, + notBefore: this.notBefore, + requestId: this.requestId, + resources: this.resources, + }; + } + + private buildMessage(): string { + return createSiweMessage({ + ...(this.scheme ? { scheme: this.scheme } : {}), + domain: this.domain, + address: viemGetAddress(this.address), + ...(this.statement ? { statement: this.statement } : {}), + uri: this.uri, + version: this.version as '1', + chainId: this.chainId, + nonce: this.nonce, + ...(this.issuedAt ? { issuedAt: new Date(this.issuedAt) } : {}), + ...(this.expirationTime + ? { expirationTime: new Date(this.expirationTime) } + : {}), + ...(this.notBefore ? { notBefore: new Date(this.notBefore) } : {}), + ...(this.requestId ? { requestId: this.requestId } : {}), + ...(this.resources ? { resources: this.resources } : {}), + }); + } + private async verifyEnsAddress(ens: string, address: string) { const resolvedAddress = await this.provider.getEnsAddress({ name: normalize(ens), diff --git a/packages/@justaname.id/siwens/src/lib/types/index.ts b/packages/@justaname.id/siwens/src/lib/types/index.ts new file mode 100644 index 00000000..97f5bb99 --- /dev/null +++ b/packages/@justaname.id/siwens/src/lib/types/index.ts @@ -0,0 +1,88 @@ +/** + * Local, ethers-free replacements for the SIWE types that used to be imported + * from the `siwe` package. Keeping the same shapes (and the same `SiweError` + * `type` strings) preserves the public API and the error contract that the + * SDK's sign-in flow and downstream consumers depend on. + */ + +/** EIP-4361 message fields, mirroring the public surface of `siwe`'s SiweMessage. */ +export interface SiweMessageFields { + scheme?: string; + domain: string; + address: string; + statement?: string; + uri: string; + version: string; + chainId: number; + nonce: string; + issuedAt?: string; + expirationTime?: string; + notBefore?: string; + requestId?: string; + resources?: string[]; +} + +/** Result returned (or thrown) by a verification. */ +export interface SiweResponse { + success: boolean; + data: SiweMessageFields; + error?: SiweError; +} + +/** Parameters accepted by `SIWENS.verify`. */ +export interface VerifyParams { + signature: string; + scheme?: string; + domain?: string; + nonce?: string; + time?: string; +} + +/** Options accepted by `SIWENS.verify`. */ +export interface VerifyOpts { + suppressExceptions?: boolean; +} + +/** + * Mirrors `siwe`'s SiweError so thrown/returned error shapes are unchanged. + */ +export class SiweError { + constructor( + public type: SiweErrorType, + public expected?: string, + public received?: string + ) {} +} + +/** + * Possible message error types. Values are copied verbatim from `siwe` so any + * consumer matching on the message string keeps working. + */ +export enum SiweErrorType { + /** `expirationTime` is present and in the past. */ + EXPIRED_MESSAGE = 'Expired message.', + /** `domain` is not a valid authority or is empty. */ + INVALID_DOMAIN = 'Invalid domain.', + /** `scheme` don't match the scheme provided for verification. */ + SCHEME_MISMATCH = 'Scheme does not match provided scheme for verification.', + /** `domain` don't match the domain provided for verification. */ + DOMAIN_MISMATCH = 'Domain does not match provided domain for verification.', + /** `nonce` don't match the nonce provided for verification. */ + NONCE_MISMATCH = 'Nonce does not match provided nonce for verification.', + /** `address` does not conform to EIP-55 or is not a valid address. */ + INVALID_ADDRESS = 'Invalid address.', + /** `uri` does not conform to RFC 3986. */ + INVALID_URI = 'URI does not conform to RFC 3986.', + /** `nonce` is smaller then 8 characters or is not alphanumeric */ + INVALID_NONCE = 'Nonce size smaller then 8 characters or is not alphanumeric.', + /** `notBefore` is present and in the future. */ + NOT_YET_VALID_MESSAGE = 'Message is not valid yet.', + /** Signature doesn't match the address of the message. */ + INVALID_SIGNATURE = 'Signature does not match address of the message.', + /** `expirationTime`, `notBefore` or `issuedAt` not complient to ISO-8601. */ + INVALID_TIME_FORMAT = 'Invalid time format.', + /** `version` is not 1. */ + INVALID_MESSAGE_VERSION = 'Invalid message version.', + /** Thrown when some required field is missing. */ + UNABLE_TO_PARSE = 'Unable to parse the message.', +} diff --git a/packages/@justaname.id/siwens/src/lib/utils/generateNonce/index.ts b/packages/@justaname.id/siwens/src/lib/utils/generateNonce/index.ts new file mode 100644 index 00000000..dcc32914 --- /dev/null +++ b/packages/@justaname.id/siwens/src/lib/utils/generateNonce/index.ts @@ -0,0 +1,19 @@ +import { randomStringForEntropy } from '@stablelib/random'; + +/** + * Generates a cryptographically-secure, EIP-4361-compliant nonce. + * + * This mirrors `siwe`'s `generateNonce` (96 bits of entropy via a CSPRNG) so we + * keep identical nonce strength/format after dropping the `siwe` dependency. + * Intentionally NOT viem's `generateSiweNonce`, which is backed by `Math.random` + * and would be a security regression. + * + * @returns {string} A randomly generated alphanumeric nonce. + */ +export function generateNonce(): string { + const nonce = randomStringForEntropy(96); + if (!nonce || nonce.length < 8) { + throw new Error('Error during nonce creation.'); + } + return nonce; +} diff --git a/packages/@justaname.id/siwens/src/lib/utils/index.ts b/packages/@justaname.id/siwens/src/lib/utils/index.ts index 4f35d9ee..b20631c5 100644 --- a/packages/@justaname.id/siwens/src/lib/utils/index.ts +++ b/packages/@justaname.id/siwens/src/lib/utils/index.ts @@ -1,3 +1,4 @@ export * from './checkTTL' export * from './checkDomainValid' -export * from './signInStatementHelpers' \ No newline at end of file +export * from './signInStatementHelpers' +export * from './generateNonce' \ No newline at end of file diff --git a/packages/@justaname.id/siwens/src/test/siwens.format.spec.ts b/packages/@justaname.id/siwens/src/test/siwens.format.spec.ts new file mode 100644 index 00000000..16e1cb68 --- /dev/null +++ b/packages/@justaname.id/siwens/src/test/siwens.format.spec.ts @@ -0,0 +1,119 @@ +import { SIWENS, SiweErrorType } from '../'; + +/** + * CI-safe tests (no RPC required). These lock the EIP-4361 message format to be + * byte-identical to what `siwe` produced before the viem migration, and verify + * that field-mismatch checks throw the same `SiweError` types. Signature + * verification (which needs a provider) is covered by the integration tests in + * siwens.spec.ts. + */ + +const ADDRESS = '0x59c44836630760F97b74b569B379ca94c37B93ca'; +const DUMMY_SIGNATURE = '0x' + '00'.repeat(65); + +// Golden string captured from `siwe`'s SiweMessage.prepareMessage() for the +// SIWENS object-construction inputs below (statement from `alice.eth`). +const GOLDEN_MESSAGE = `localhost wants you to sign in with your Ethereum account: +0x59c44836630760F97b74b569B379ca94c37B93ca + +I am signing in with my ENS: alice.eth + +URI: http://localhost:3333 +Version: 1 +Chain ID: 1 +Nonce: abcdef1234567890 +Issued At: 2024-01-01T00:00:00.000Z +Expiration Time: 2024-01-01T00:01:00.000Z`; + +const baseParams = { + domain: 'localhost', + address: ADDRESS, + uri: 'http://localhost:3333', + version: '1', + nonce: 'abcdef1234567890', + chainId: 1, + ttl: 60 * 1000, + ens: 'alice.eth', + issuedAt: '2024-01-01T00:00:00.000Z', + expirationTime: '2024-01-01T00:01:00.000Z', +}; + +describe('SIWENS message format (golden)', () => { + it('builds a byte-identical EIP-4361 message', () => { + const siwens = new SIWENS({ params: { ...baseParams } }); + expect(siwens.prepareMessage()).toBe(GOLDEN_MESSAGE); + expect(siwens.toMessage()).toBe(GOLDEN_MESSAGE); + }); + + it('round-trips when re-parsed from the string form', () => { + const message = new SIWENS({ params: { ...baseParams } }).prepareMessage(); + const reparsed = new SIWENS({ + params: message, + providerUrl: 'http://127.0.0.1:1', + }); + expect(reparsed.address).toBe(ADDRESS); + expect(reparsed.chainId).toBe(1); + expect(reparsed.domain).toBe('localhost'); + expect(reparsed.nonce).toBe('abcdef1234567890'); + expect(reparsed.statement).toBe('I am signing in with my ENS: alice.eth'); + }); +}); + +describe('SIWENS.verify field validation (no RPC)', () => { + it('throws DOMAIN_MISMATCH with the preserved error shape', async () => { + const siwens = new SIWENS({ params: { ...baseParams } }); + await expect( + siwens.verify({ signature: DUMMY_SIGNATURE, domain: 'evil.com' }) + ).rejects.toMatchObject({ + success: false, + error: { type: SiweErrorType.DOMAIN_MISMATCH }, + ens: 'alice.eth', + }); + }); + + it('throws NONCE_MISMATCH', async () => { + const siwens = new SIWENS({ params: { ...baseParams } }); + await expect( + siwens.verify({ signature: DUMMY_SIGNATURE, nonce: 'someOtherNonce123' }) + ).rejects.toMatchObject({ + error: { type: SiweErrorType.NONCE_MISMATCH }, + }); + }); + + it('throws EXPIRED_MESSAGE when the message is past expiry', async () => { + const siwens = new SIWENS({ + params: { + ...baseParams, + issuedAt: '2020-01-01T00:00:00.000Z', + expirationTime: '2020-01-01T00:01:00.000Z', + }, + }); + await expect( + siwens.verify({ signature: DUMMY_SIGNATURE }) + ).rejects.toMatchObject({ + error: { type: SiweErrorType.EXPIRED_MESSAGE }, + }); + }); + + it('returns a failure result instead of throwing when suppressExceptions is set', async () => { + const siwens = new SIWENS({ params: { ...baseParams } }); + const result = await siwens.verify( + { signature: DUMMY_SIGNATURE, domain: 'evil.com' }, + { suppressExceptions: true } + ); + expect(result.success).toBe(false); + expect(result.error?.type).toBe(SiweErrorType.DOMAIN_MISMATCH); + expect(result.ens).toBe('alice.eth'); + }); +}); + +describe('generateNonce', () => { + it('produces alphanumeric nonces of sufficient length', () => { + const nonce = SIWENS.generateNonce(); + expect(nonce).toMatch(/^[a-zA-Z0-9]{8,}$/); + }); + + it('produces a different nonce each call', () => { + expect(SIWENS.generateNonce()).not.toBe(SIWENS.generateNonce()); + }); +}); diff --git a/yarn.lock b/yarn.lock index d9a439d4..92773d43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4487,7 +4487,6 @@ __metadata: jest: "npm:^29.4.1" qs: "npm:6.12.0" peerDependencies: - siwe: ">=2.0.0" viem: ^2.48.0 languageName: unknown linkType: soft @@ -4496,9 +4495,9 @@ __metadata: version: 0.0.0-use.local resolution: "@justaname.id/siwens@workspace:packages/@justaname.id/siwens" dependencies: + "@stablelib/random": "npm:^1.0.2" punycode: "npm:^2.3.1" peerDependencies: - siwe: ">=2.0.0" viem: ^2.48.0 languageName: unknown linkType: soft @@ -5640,7 +5639,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.7.1, @noble/hashes@npm:^1.1.2, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.2, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.5.0, @noble/hashes@npm:~1.7.1": +"@noble/hashes@npm:1.7.1, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.2, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.5.0, @noble/hashes@npm:~1.7.1": version: 1.7.1 resolution: "@noble/hashes@npm:1.7.1" checksum: 10c0/2f8ec0338ccc92b576a0f5c16ab9c017a3a494062f1fbb569ae641c5e7eab32072f9081acaa96b5048c0898f972916c818ea63cbedda707886a4b5ffcfbf94e3 @@ -9277,18 +9276,6 @@ __metadata: languageName: node linkType: hard -"@spruceid/siwe-parser@npm:^2.1.2": - version: 2.1.2 - resolution: "@spruceid/siwe-parser@npm:2.1.2" - dependencies: - "@noble/hashes": "npm:^1.1.2" - apg-js: "npm:^4.3.0" - uri-js: "npm:^4.4.1" - valid-url: "npm:^1.0.9" - checksum: 10c0/79005ae8978b9dd0c1ece949dbc2294d6a641db757c14ae0864b6803358cc498bac882d8031e656b4dbf3e838be043ce6517c857b6e2df26a1e8922baeb2c07d - languageName: node - linkType: hard - "@stablelib/binary@npm:^1.0.1": version: 1.0.1 resolution: "@stablelib/binary@npm:1.0.1" @@ -9305,7 +9292,7 @@ __metadata: languageName: node linkType: hard -"@stablelib/random@npm:^1.0.1": +"@stablelib/random@npm:^1.0.2": version: 1.0.2 resolution: "@stablelib/random@npm:1.0.2" dependencies: @@ -14270,13 +14257,6 @@ __metadata: languageName: node linkType: hard -"apg-js@npm:^4.3.0": - version: 4.4.0 - resolution: "apg-js@npm:4.4.0" - checksum: 10c0/b3e60e2ba8b25fe1c9fcc648f43b98f02f0eff3bbd593fd2866302fe57b1b7840ee9be894ebed6214876a6feecd543cc717d7b68351bf2df831db110ae01e6bb - languageName: node - linkType: hard - "app-root-dir@npm:^1.0.2": version: 1.0.2 resolution: "app-root-dir@npm:1.0.2" @@ -25427,7 +25407,6 @@ __metadata: rollup-plugin-tailwindcss: "npm:1.0.0" rollup-plugin-typescript2: "npm:0.36.0" rollup-preserve-directives: "npm:1.1.1" - siwe: "npm:2.3.2" storybook: "npm:8.2.8" tailwind-merge: "npm:2.5.2" tailwindcss: "npm:3.4.3" @@ -33040,20 +33019,6 @@ __metadata: languageName: node linkType: hard -"siwe@npm:2.3.2": - version: 2.3.2 - resolution: "siwe@npm:2.3.2" - dependencies: - "@spruceid/siwe-parser": "npm:^2.1.2" - "@stablelib/random": "npm:^1.0.1" - uri-js: "npm:^4.4.1" - valid-url: "npm:^1.0.9" - peerDependencies: - ethers: ^5.6.8 || ^6.0.8 - checksum: 10c0/05ee09cdabef72a8ec54ffe24e517c386eb49bb6385ffc7ec159e266b3661a98405ca88b8e75278b376754e943a5118790c2add98b5012e1c4ec13bce4e6ee03 - languageName: node - linkType: hard - "slash@npm:3.0.0, slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -35798,7 +35763,7 @@ __metadata: languageName: node linkType: hard -"uri-js@npm:^4.2.2, uri-js@npm:^4.4.1": +"uri-js@npm:^4.2.2": version: 4.4.1 resolution: "uri-js@npm:4.4.1" dependencies: @@ -35998,13 +35963,6 @@ __metadata: languageName: node linkType: hard -"valid-url@npm:^1.0.9": - version: 1.0.9 - resolution: "valid-url@npm:1.0.9" - checksum: 10c0/3995e65f9942dbcb1621754c0f9790335cec61e9e9310c0a809e9ae0e2ae91bb7fc6a471fba788e979db0418d9806639f681ecebacc869bc8c3de88efa562ee6 - languageName: node - linkType: hard - "validate-npm-package-license@npm:^3.0.1, validate-npm-package-license@npm:^3.0.4": version: 3.0.4 resolution: "validate-npm-package-license@npm:3.0.4"