diff --git a/apps/wallet/src/ui/app/background-client/index.ts b/apps/wallet/src/ui/app/background-client/index.ts index 17ae153b135d0..e0b52bcb5a874 100644 --- a/apps/wallet/src/ui/app/background-client/index.ts +++ b/apps/wallet/src/ui/app/background-client/index.ts @@ -25,6 +25,10 @@ import { setActiveOrigin, changeActiveNetwork } from '_redux/slices/app'; import { setPermissions } from '_redux/slices/permissions'; import { setTransactionRequests } from '_redux/slices/transaction-requests'; import { type SerializedLedgerAccount } from '_src/background/keyring/LedgerAccount'; +import { + isQredoConnectPayload, + type QredoConnectPayload, +} from '_src/shared/messaging/messages/payloads/QredoConnect'; import type { SuiAddress, SuiTransactionBlockResponse } from '@mysten/sui.js'; import type { Message } from '_messages'; @@ -364,6 +368,85 @@ export class BackgroundClient { ); } + public fetchPendingQredoConnectRequest(requestID: string) { + return lastValueFrom( + this.sendMessage( + createMessage>({ + type: 'qredo-connect', + method: 'getPendingRequest', + args: { requestID }, + }) + ).pipe( + take(1), + map(({ payload }) => { + if ( + isQredoConnectPayload( + payload, + 'getPendingRequestResponse' + ) + ) { + return payload.args.request; + } + throw new Error( + 'Error unknown response for fetch pending qredo requests message' + ); + }) + ) + ); + } + + public getQredoConnectionInfo(qredoID: string, refreshAccessToken = false) { + return lastValueFrom( + this.sendMessage( + createMessage>({ + type: 'qredo-connect', + method: 'getQredoInfo', + args: { qredoID, refreshAccessToken }, + }) + ).pipe( + take(1), + map(({ payload }) => { + if ( + isQredoConnectPayload(payload, 'getQredoInfoResponse') + ) { + return payload.args; + } + throw new Error( + 'Error unknown response for get qredo info message' + ); + }) + ) + ); + } + + public acceptQredoConnection( + args: QredoConnectPayload<'acceptQredoConnection'>['args'] + ) { + return lastValueFrom( + this.sendMessage( + createMessage>({ + type: 'qredo-connect', + method: 'acceptQredoConnection', + args, + }) + ).pipe(take(1)) + ); + } + + public rejectQredoConnection( + args: QredoConnectPayload<'rejectQredoConnection'>['args'] + ) { + return lastValueFrom( + this.sendMessage( + createMessage>({ + type: 'qredo-connect', + method: 'rejectQredoConnection', + args, + }) + ).pipe(take(1)) + ); + } + private setupAppStatusUpdateInterval() { setInterval(() => { this.sendAppStatus(); diff --git a/apps/wallet/src/ui/app/helpers/queryClient.ts b/apps/wallet/src/ui/app/helpers/queryClient.ts index 6cb3aa891485b..5496fb9df5e15 100644 --- a/apps/wallet/src/ui/app/helpers/queryClient.ts +++ b/apps/wallet/src/ui/app/helpers/queryClient.ts @@ -30,7 +30,7 @@ export const queryClient = new QueryClient({ function createIDBPersister(idbValidKey: IDBValidKey) { return { persistClient: async (client: PersistedClient) => { - set(idbValidKey, client); + await set(idbValidKey, client); }, restoreClient: async () => { return await get(idbValidKey); diff --git a/apps/wallet/src/ui/app/hooks/useQredoAPI.ts b/apps/wallet/src/ui/app/hooks/useQredoAPI.ts new file mode 100644 index 0000000000000..4217647444f3c --- /dev/null +++ b/apps/wallet/src/ui/app/hooks/useQredoAPI.ts @@ -0,0 +1,55 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { useEffect, useState } from 'react'; + +import { useBackgroundClient } from './useBackgroundClient'; +import { useQredoInfo } from './useQredoInfo'; +import { QredoAPI } from '_src/shared/qredo-api'; + +const API_INSTANCES: Record = {}; + +export function useQredoAPI(qredoID?: string) { + const backgroundClient = useBackgroundClient(); + const { data, isLoading, error } = useQredoInfo(qredoID); + const [api, setAPI] = useState( + () => (qredoID && API_INSTANCES[qredoID]) || null + ); + useEffect(() => { + if ( + data?.qredoInfo?.apiUrl && + data?.qredoInfo?.accessToken && + qredoID + ) { + const instance = API_INSTANCES[qredoID]; + if ( + instance && + instance.accessToken !== data.qredoInfo.accessToken + ) { + instance.accessToken = data.qredoInfo.accessToken; + } else if (!instance) { + API_INSTANCES[qredoID] = new QredoAPI( + qredoID, + data.qredoInfo.apiUrl, + { + accessTokenRenewalFN: async (qredoID) => + ( + await backgroundClient.getQredoConnectionInfo( + qredoID, + true + ) + ).qredoInfo?.accessToken || null, + accessToken: data.qredoInfo.accessToken, + } + ); + } + } + setAPI((qredoID && API_INSTANCES[qredoID]) || null); + }, [ + backgroundClient, + data?.qredoInfo?.apiUrl, + data?.qredoInfo?.accessToken, + qredoID, + ]); + return [api, isLoading, error] as const; +} diff --git a/apps/wallet/src/ui/app/hooks/useQredoInfo.ts b/apps/wallet/src/ui/app/hooks/useQredoInfo.ts new file mode 100644 index 0000000000000..f8b7c9b4c7786 --- /dev/null +++ b/apps/wallet/src/ui/app/hooks/useQredoInfo.ts @@ -0,0 +1,18 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { useQuery } from '@tanstack/react-query'; + +import { useBackgroundClient } from './useBackgroundClient'; + +export function useQredoInfo(qredoID?: string) { + const backgroundClient = useBackgroundClient(); + return useQuery({ + queryKey: ['qredo', 'info', qredoID], + queryFn: async () => backgroundClient.getQredoConnectionInfo(qredoID!), + enabled: !!qredoID, + staleTime: 0, + refetchInterval: 1000, + meta: { skipPersistedCache: true }, + }); +} diff --git a/apps/wallet/src/ui/app/pages/qredo-connect/hooks.ts b/apps/wallet/src/ui/app/pages/qredo-connect/hooks.ts new file mode 100644 index 0000000000000..78537cfa2a567 --- /dev/null +++ b/apps/wallet/src/ui/app/pages/qredo-connect/hooks.ts @@ -0,0 +1,45 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { useQuery } from '@tanstack/react-query'; + +import { useBackgroundClient } from '../../hooks/useBackgroundClient'; +import { useQredoAPI } from '../../hooks/useQredoAPI'; +import { type GetWalletsParams } from '_src/shared/qredo-api'; + +export function useQredoUIPendingRequest(requestID?: string) { + const backgroundClient = useBackgroundClient(); + return useQuery({ + queryKey: ['qredo-connect', 'pending-request', requestID], + queryFn: async () => + await backgroundClient.fetchPendingQredoConnectRequest(requestID!), + staleTime: 0, + refetchInterval: 1000, + enabled: !!requestID, + meta: { skipPersistedCache: true }, + }); +} + +export function useFetchQredoAccounts( + qredoID: string, + enabled?: boolean, + params?: GetWalletsParams +) { + const [api, isAPILoading, apiInitError] = useQredoAPI(qredoID); + return useQuery({ + queryKey: ['qredo', 'fetch', 'accounts', qredoID, api, apiInitError], + queryFn: async () => { + if (api) { + return (await api.getWallets(params)).wallets; + } + throw apiInitError + ? apiInitError + : new Error('Qredo API initialization failed'); + }, + enabled: + !!qredoID && + (enabled ?? true) && + !isAPILoading && + !!(api || apiInitError), + }); +} diff --git a/apps/wallet/src/ui/app/pages/qredo-connect/utils.ts b/apps/wallet/src/ui/app/pages/qredo-connect/utils.ts new file mode 100644 index 0000000000000..d4b450b3a63e2 --- /dev/null +++ b/apps/wallet/src/ui/app/pages/qredo-connect/utils.ts @@ -0,0 +1,18 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { type UIQredoPendingRequest } from '_src/background/qredo/types'; + +export function isUntrustedQredoConnect({ + apiUrl, + origin, +}: UIQredoPendingRequest) { + try { + return ( + new URL(origin).protocol !== 'https:' || + new URL(apiUrl).protocol !== 'https:' + ); + } catch (e) { + return false; + } +} diff --git a/apps/wallet/src/ui/index.tsx b/apps/wallet/src/ui/index.tsx index 90ad04b005e2a..f9a5d5154e96d 100644 --- a/apps/wallet/src/ui/index.tsx +++ b/apps/wallet/src/ui/index.tsx @@ -69,7 +69,13 @@ function AppWrapper() { + !meta?.skipPersistedCache, + }, + }} >