From a9f9e118960be3383bf7ff424842aace77c89dca Mon Sep 17 00:00:00 2001 From: Javad Khalilian Date: Fri, 24 Jan 2025 15:56:52 +0100 Subject: [PATCH] feat(dw): add sign requests for plugins (#2829) * feat(dw): add sign requests for plugins * fix(dw): dapp connection * refactor(dw): communication * fix(dw): acrivity table spacing * feat(dw): reject sign request --- .../apps/dev-wallet-example/src/app/page.tsx | 38 +++--- .../public/hosted-plugins/plugins.json | 2 +- .../src/App/Layout/useRightAside.ts | 10 +- packages/apps/dev-wallet/src/App/routes.tsx | 11 +- .../communication/communication.provider.tsx | 112 +++++++++++++----- .../account/Components/ActivityTable.tsx | 24 ++-- .../dev-wallet/src/pages/connect/connect.tsx | 22 +++- .../plugins/PluginCommunicationProvider.tsx | 110 +++++++++++++++++ .../plugins/components/ConnectionRequest.tsx | 40 +++++++ .../plugins/components/SignRequestDialog.tsx | 30 +++++ .../dev-wallet/src/pages/plugins/plugins.tsx | 73 ++++++------ .../dev-wallet/src/pages/plugins/type.d.ts | 10 ++ .../src/pages/transaction/sign-request.tsx | 58 ++++++++- 13 files changed, 432 insertions(+), 108 deletions(-) create mode 100644 packages/apps/dev-wallet/src/pages/plugins/PluginCommunicationProvider.tsx create mode 100644 packages/apps/dev-wallet/src/pages/plugins/components/ConnectionRequest.tsx create mode 100644 packages/apps/dev-wallet/src/pages/plugins/components/SignRequestDialog.tsx create mode 100644 packages/apps/dev-wallet/src/pages/plugins/type.d.ts diff --git a/packages/apps/dev-wallet-example/src/app/page.tsx b/packages/apps/dev-wallet-example/src/app/page.tsx index 3bbce82b39..d419299e00 100644 --- a/packages/apps/dev-wallet-example/src/app/page.tsx +++ b/packages/apps/dev-wallet-example/src/app/page.tsx @@ -17,8 +17,7 @@ const sleep = (time: number) => }, time); }); -const walletOrigin = () => - (window as any).walletUrl || 'https://wallet.kadena.io'; +const walletOrigin = () => (window as any).walletUrl || 'http://localhost:4173'; const walletUrl = () => `${walletOrigin()}`; const walletName = 'Dev-Wallet'; const appName = 'Dev Wallet Example'; @@ -114,9 +113,7 @@ interface IState { }; accounts: Array<{ address: string; - keyset: { - guard: { keys: string[]; pred: 'keys-all' | 'keys-any' | 'keys-2' }; - }; + guard: { keys: string[]; pred: 'keys-all' | 'keys-any' | 'keys-2' }; alias: string; contract: string; chains: Array<{ chainId: ChainId; balance: string }>; @@ -166,7 +163,7 @@ export default function Home() { }); addLog(status); setProfile((status.payload as any).profile); - // close(); + close(); }} > {profile @@ -189,7 +186,8 @@ export default function Home() { }); addLog(response); setState(response.payload as IState); - // close(); + console.log(response.payload); + close(); }} > GET_STATUS @@ -209,7 +207,7 @@ export default function Home() {
Alias: {account.alias}
Contract: {account.contract}
overallBalance: {account.overallBalance}
-
Guard: {JSON.stringify(account.keyset.guard)}
+
Guard: {JSON.stringify(account.guard)}
))} @@ -223,34 +221,40 @@ export default function Home() { onClick={async () => { if (!state) return; const { message, focus, close } = await getWalletConnection(); + const accounts = state.accounts.filter( + ({ overallBalance }) => +overallBalance > 0, + ); + if (accounts.length < 2) { + setLog(['Not enough accounts with balance']); + } const tx = transferAllCommand({ sender: { - account: state.accounts[0].address, - publicKeys: state.accounts[0].keyset.guard.keys, + account: accounts[0].address, + publicKeys: accounts[0].guard.keys, }, receiver: { - account: state.accounts[1].address, - keyset: state.accounts[1].keyset.guard, + account: accounts[1].address, + keyset: accounts[1].guard, }, - chainId: state.accounts[0].chains[0].chainId, - amount: state.accounts[0].chains[0].balance, - contract: state.accounts[0].contract, + chainId: accounts[0].chains[0].chainId, + amount: accounts[0].chains[0].balance, + contract: accounts[0].contract, }); focus(); const response = await message( 'SIGN_REQUEST', createTransaction(tx()) as any, ); + console.log(response); const payload: { status: 'signed' | 'rejected'; transaction?: ICommand; } = response.payload as any; - debugger; if (payload && payload.status === 'signed') { setSignedTx(payload.transaction as ICommand); } addLog(response); - // close(); + close(); }} > Transfer balance from account 1 to account 2 diff --git a/packages/apps/dev-wallet/public/hosted-plugins/plugins.json b/packages/apps/dev-wallet/public/hosted-plugins/plugins.json index 188c1a9bea..3b318df6c8 100644 --- a/packages/apps/dev-wallet/public/hosted-plugins/plugins.json +++ b/packages/apps/dev-wallet/public/hosted-plugins/plugins.json @@ -5,7 +5,7 @@ "shortName": "Pact Console", "description": "A console for interacting remotely with Pact on different networks (read-only)", "permissions": [ - "network-list" + "GET_NETWORK_LIST" ] } ] \ No newline at end of file diff --git a/packages/apps/dev-wallet/src/App/Layout/useRightAside.ts b/packages/apps/dev-wallet/src/App/Layout/useRightAside.ts index 9b5707a93a..ce98a08599 100644 --- a/packages/apps/dev-wallet/src/App/Layout/useRightAside.ts +++ b/packages/apps/dev-wallet/src/App/Layout/useRightAside.ts @@ -1,5 +1,5 @@ import { useLayout } from '@kadena/kode-ui/patterns'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; export function useRightAside() { const [expanded, setExpanded] = useState(false); @@ -13,18 +13,18 @@ export function useRightAside() { return [ expanded, - () => { + useCallback(() => { if (isRightAsideExpanded && !expanded) { throw new Error('Right aside is already open with a different panel'); } setIsRightAsideExpanded(true); setExpanded(true); - }, - () => { + }, [isRightAsideExpanded, expanded, setIsRightAsideExpanded]), + useCallback(() => { if (expanded) { setIsRightAsideExpanded(false); setExpanded(false); } - }, + }, [expanded, setIsRightAsideExpanded]), ] as [isExpanded: boolean, expand: () => void, close: () => void]; } diff --git a/packages/apps/dev-wallet/src/App/routes.tsx b/packages/apps/dev-wallet/src/App/routes.tsx index 7028f92b53..dadb9d98c2 100644 --- a/packages/apps/dev-wallet/src/App/routes.tsx +++ b/packages/apps/dev-wallet/src/App/routes.tsx @@ -15,7 +15,7 @@ import { AccountDiscovery } from '@/pages/account-discovery/account-dsicovery'; import { AccountPage } from '@/pages/account/account'; import { MigrateAccount } from '@/pages/account/migrate-account/migrate-account'; import { ActivitiesPage } from '@/pages/activities/activities'; -import { Connect } from '@/pages/connect/connect'; +import { ConnectPage } from '@/pages/connect/connect'; import { Contacts } from '@/pages/contacts/contacts'; import { CreateAccount } from '@/pages/create-account/create-account'; import { FungiblePage } from '@/pages/fungible/fungible'; @@ -31,7 +31,7 @@ import { KeepPasswordPolicy } from '@/pages/settings/keep-password-policy/keep-p import { RevealPhrase } from '@/pages/settings/reveal-phrase/reveal-phrase'; import { Settings } from '@/pages/settings/settings'; import { SignatureBuilder } from '@/pages/signature-builder/signature-builder'; -import { SignRequest } from '@/pages/transaction/sign-request'; +import { SignRequestPage } from '@/pages/transaction/sign-request'; import { TransactionPage } from '@/pages/transaction/Transaction'; import { Transfer } from '@/pages/transfer/transfer'; import { ImportChainweaverExport } from '@/pages/wallet-recovery/import-chainweaver-export/import-chainweaver-export'; @@ -119,7 +119,7 @@ export const Routes: FC = () => { } /> } /> } /> - } /> + } /> } /> } /> { > } /> - } /> + } + /> }> diff --git a/packages/apps/dev-wallet/src/modules/communication/communication.provider.tsx b/packages/apps/dev-wallet/src/modules/communication/communication.provider.tsx index 852cd046e8..c9080573be 100644 --- a/packages/apps/dev-wallet/src/modules/communication/communication.provider.tsx +++ b/packages/apps/dev-wallet/src/modules/communication/communication.provider.tsx @@ -4,25 +4,27 @@ import { FC, PropsWithChildren, createContext, + useCallback, useContext, useEffect, useState, } from 'react'; import { useWallet } from '../wallet/wallet.hook'; -type Message = { +export type Message = { id: string; type: RequestType; payload: unknown; }; -type RequestType = +export type UiRequiredRequest = 'CONNECTION_REQUEST' | 'SIGN_REQUEST'; + +export type RequestType = + | UiRequiredRequest | 'GET_STATUS' - | 'CONNECTION_REQUEST' - | 'SIGN_REQUEST' - | 'PAYMENT_REQUEST' - | 'UNLOCK_REQUEST' - | 'GET_NETWORK_LIST'; + | 'GET_NETWORK_LIST' + | 'GET_ACCOUNTS'; + type Request = Message & { resolve: (data: unknown) => void; reject: (error: unknown) => void; @@ -35,14 +37,13 @@ const messageHandle = ( ) => Promise<{ payload: unknown } | { error: unknown }>, ) => { const cb = async (event: MessageEvent) => { - if (event.data.type === type && event.source) { + if (event.data.type === type && event.source && event.origin !== 'null') { const payload = await handler(event.data); event.source.postMessage( { id: event.data.id, type: event.data.type, ...payload }, - // TODO: use sessionId of plugins, since 'null' happens for the iframe plugins that we need more proper handling - { targetOrigin: event.origin === 'null' ? '*' : event.origin }, + { targetOrigin: event.origin }, ); - if (window.opener && event.origin && event.origin !== 'null') { + if (window.opener && event.origin) { window.opener.focus(); } } @@ -71,23 +72,38 @@ export const CommunicationProvider: FC< } >, ) => () => void; - uiLoader?: (route: string) => void; + uiLoader?: (requestId: string, requestType: UiRequiredRequest) => void; }> > = ({ children, handle = messageHandle, uiLoader }) => { const { setOrigin } = useGlobalState(); const [requests] = useState(() => new Map()); const routeNavigate = usePatchedNavigate(); - const navigate = - uiLoader || - ((route: string) => { - setOrigin(route); - routeNavigate(route); - }); - const { isUnlocked, accounts, profile, networks, activeNetwork } = + const defaultUiLoader = useCallback( + (requestId: string, requestType: UiRequiredRequest) => { + const routeMap = { + CONNECTION_REQUEST: '/connect', + PAYMENT_REQUEST: '/payment', + SIGN_REQUEST: '/sign-request', + }; + const route = routeMap[requestType]; + if (!route) return; + const path = `${route}/${requestId}`; + setOrigin(path); + routeNavigate(path); + }, + [routeNavigate, setOrigin], + ); + const loadUiComponent = useCallback( + (requestId: string, requestType: UiRequiredRequest) => { + const loader = uiLoader || defaultUiLoader; + return loader(requestId, requestType); + }, + [defaultUiLoader, uiLoader], + ); + const { isUnlocked, accounts, profile, networks, activeNetwork, keySources } = useWallet(); useEffect(() => { - console.log('CommunicationProvider mounted', isUnlocked); const createRequest = (data: Message) => new Promise<{ payload: unknown } | { error: unknown }>((resolve) => { const request = { @@ -108,23 +124,44 @@ export const CommunicationProvider: FC< requests.delete(data.id); }); - const handleRequest = (type: RequestType, route: string) => + const handleUIRequiredRequest = (type: UiRequiredRequest) => handle(type, async (payload) => { - console.log('handleRequest', type, payload); const request = createRequest(payload); - setOrigin(`${route}/${payload.id}`); - navigate(`${route}/${payload.id}`); + loadUiComponent(payload.id, type); return request; }); const handlers = [ - handleRequest('CONNECTION_REQUEST', '/connect'), - handleRequest('PAYMENT_REQUEST', '/payment'), - handleRequest('SIGN_REQUEST', '/sign-request'), + handleUIRequiredRequest('CONNECTION_REQUEST'), + handleUIRequiredRequest('SIGN_REQUEST'), handle('GET_STATUS', async () => { + if (!isUnlocked || !profile) return { payload: { isUnlocked: false } }; + const { uuid, name, accentColor } = profile; + const accountsToSend = accounts.map( + ({ address, alias, overallBalance, chains, guard }) => ({ + address, + alias, + overallBalance, + chains, + guard, + }), + ); return { payload: { isUnlocked: isUnlocked, - ...(isUnlocked ? { profile, accounts } : {}), + profile: { + uuid, + name, + accentColor, + authMode: profile.options?.authMode, + }, + accounts: accountsToSend, + keySources: keySources.map(({ uuid, source, keys }) => ({ + uuid, + source, + keys, + })), + networks, + activeNetwork, }, }; }), @@ -133,12 +170,27 @@ export const CommunicationProvider: FC< payload: isUnlocked ? networks : [], }; }), + handle('GET_ACCOUNTS', async () => { + return { + payload: isUnlocked + ? accounts.map( + ({ address, alias, overallBalance, chains, guard }) => ({ + address, + alias, + overallBalance, + chains, + guard, + }), + ) + : [], + }; + }), ]; return () => { handlers.forEach((unsubscribe) => unsubscribe()); }; }, [ - navigate, + loadUiComponent, requests, isUnlocked, accounts, @@ -146,6 +198,8 @@ export const CommunicationProvider: FC< networks, activeNetwork, setOrigin, + handle, + keySources, ]); return ( diff --git a/packages/apps/dev-wallet/src/pages/account/Components/ActivityTable.tsx b/packages/apps/dev-wallet/src/pages/account/Components/ActivityTable.tsx index 0fb1f3bd9c..a10627e1bd 100644 --- a/packages/apps/dev-wallet/src/pages/account/Components/ActivityTable.tsx +++ b/packages/apps/dev-wallet/src/pages/account/Components/ActivityTable.tsx @@ -1,7 +1,7 @@ import { noStyleLinkClass } from '@/Components/Accounts/style.css'; import { IActivity } from '@/modules/activity/activity.repository'; import { shorten } from '@/utils/helpers'; -import { Text } from '@kadena/kode-ui'; +import { Stack, Text } from '@kadena/kode-ui'; import { CompactTable, usePagination } from '@kadena/kode-ui/patterns'; import { useMemo } from 'react'; import { Link } from 'react-router-dom'; @@ -40,7 +40,7 @@ export function ActivityTable({ activities }: { activities: IActivity[] }) { ), receivers: activity.data.transferData.receivers .map((receiver) => shorten(receiver?.address ?? '', 6)) - .join(' | '), + .join(' '), })), [activities], ); @@ -55,7 +55,7 @@ export function ActivityTable({ activities }: { activities: IActivity[] }) { { label: 'Id', key: 'open', - width: '10%', + width: '20%', variant: 'code', render: ({ value }) => value, }, @@ -63,26 +63,34 @@ export function ActivityTable({ activities }: { activities: IActivity[] }) { label: 'Type', key: 'type', variant: 'code', - width: '10%', + width: '20%', }, { label: 'Sender', key: 'sender', variant: 'code', - width: '10%', + width: '20%', }, { label: 'Amount', key: 'amount', variant: 'code', - width: '10%', - align: 'end', + width: '20%', }, { label: 'Receivers', key: 'receivers', variant: 'code', - width: '70%', + width: '20%', + render: ({ value }) => ( + + {value.split(' ').map((address) => ( + + {address} + + ))} + + ), }, ]} data={data} diff --git a/packages/apps/dev-wallet/src/pages/connect/connect.tsx b/packages/apps/dev-wallet/src/pages/connect/connect.tsx index 94a3530c24..7095c81260 100644 --- a/packages/apps/dev-wallet/src/pages/connect/connect.tsx +++ b/packages/apps/dev-wallet/src/pages/connect/connect.tsx @@ -3,9 +3,16 @@ import { Button, Heading, Notification, Stack, Text } from '@kadena/kode-ui'; import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; -export function Connect() { +export function Connect({ + requestId, + onAccept, + onReject, +}: { + requestId?: string; + onAccept?: () => void; + onReject?: () => void; +}) { const requests = useRequests(); - const { requestId } = useParams(); const [result, setResult] = useState<'none' | 'rejected' | 'accepted'>( 'none', ); @@ -32,6 +39,9 @@ export function Connect() { onPress={() => { request?.resolve({ status: 'accepted' }); setResult('accepted'); + if (onAccept) { + onAccept(); + } }} >
Accept
@@ -40,6 +50,9 @@ export function Connect() { onPress={() => { request?.resolve({ status: 'rejected' }); setResult('rejected'); + if (onReject) { + onReject(); + } }} >
reject
@@ -61,3 +74,8 @@ export function Connect() { ); } + +export const ConnectPage = () => { + const { requestId } = useParams(); + return ; +}; diff --git a/packages/apps/dev-wallet/src/pages/plugins/PluginCommunicationProvider.tsx b/packages/apps/dev-wallet/src/pages/plugins/PluginCommunicationProvider.tsx new file mode 100644 index 0000000000..3c437c23f0 --- /dev/null +++ b/packages/apps/dev-wallet/src/pages/plugins/PluginCommunicationProvider.tsx @@ -0,0 +1,110 @@ +import { + CommunicationProvider, + Message, + RequestType, +} from '@/modules/communication/communication.provider'; +import { ReactNode, useMemo, useState } from 'react'; +import { ConnectionRequest } from './components/ConnectionRequest'; +import { SignRequestDialog } from './components/SignRequestDialog'; +import { Plugin } from './type'; + +const messageHandle = + (activeSessions: string[], permissions: RequestType[]) => + ( + type: RequestType, + handler: ( + message: Message, + ) => Promise<{ payload: unknown } | { error: unknown }>, + ) => { + const cb = async (event: MessageEvent) => { + if ( + event.data.type === type && + event.source && + event.origin === 'null' && + event.data.sessionId + ) { + if (!permissions.includes(type)) { + event.source.postMessage( + { + id: event.data.id, + type: event.data.type, + error: + 'PERMISSION_DENIED: the plugin does not have the permission to perform this action', + }, + { targetOrigin: '*' }, + ); + return; + } + if (!activeSessions.includes(event.data.sessionId)) { + event.source.postMessage( + { + id: event.data.id, + type: event.data.type, + error: + 'SESSION_NOT_FOUND: the session is not active in the plugin', + }, + { targetOrigin: '*' }, + ); + return; + } + const payload = await handler(event.data); + event.source.postMessage( + { id: event.data.id, type: event.data.type, ...payload }, + // TODO: use sessionId of plugins, since 'null' happens for the iframe plugins that we need more proper handling + { targetOrigin: '*' }, + ); + } + }; + window.addEventListener('message', cb); + return () => window.removeEventListener('message', cb); + }; + +export function PluginCommunicationProvider({ + children, + sessionId, + plugin, +}: { + children: ReactNode; + sessionId: string; + plugin: Plugin; +}) { + const handle = useMemo( + () => messageHandle([sessionId], plugin.permissions), + [sessionId, plugin.permissions], + ); + const [uiComponent, setUiComponent] = useState(null); + return ( + { + if (type === 'CONNECTION_REQUEST') { + setUiComponent( + { + setUiComponent(null); + }} + />, + ); + return; + } + if (type === 'SIGN_REQUEST') { + setUiComponent( + { + setUiComponent(null); + }} + />, + ); + return; + } + }} + > + {children} + {uiComponent} + + ); +} diff --git a/packages/apps/dev-wallet/src/pages/plugins/components/ConnectionRequest.tsx b/packages/apps/dev-wallet/src/pages/plugins/components/ConnectionRequest.tsx new file mode 100644 index 0000000000..97d56d307e --- /dev/null +++ b/packages/apps/dev-wallet/src/pages/plugins/components/ConnectionRequest.tsx @@ -0,0 +1,40 @@ +import { Connect } from '@/pages/connect/connect'; +import { + Dialog, + DialogHeader, + DialogHeaderSubtitle, + Stack, + Text, +} from '@kadena/kode-ui'; +import { useState } from 'react'; +import { Plugin } from '../type'; + +export function ConnectionRequest({ + requestId, + plugin, + onDone, +}: { + requestId: string; + plugin: Plugin; + onDone: () => void; +}) { + const [isOpened, setIsOpened] = useState(true); + const close = () => { + setIsOpened(false); + onDone(); + }; + return ( + + Connection Request + + + + {plugin.name} + {' '} + wants to connect to your wallet + + + + + ); +} diff --git a/packages/apps/dev-wallet/src/pages/plugins/components/SignRequestDialog.tsx b/packages/apps/dev-wallet/src/pages/plugins/components/SignRequestDialog.tsx new file mode 100644 index 0000000000..05bfdc7c25 --- /dev/null +++ b/packages/apps/dev-wallet/src/pages/plugins/components/SignRequestDialog.tsx @@ -0,0 +1,30 @@ +import { SignRequest } from '@/pages/transaction/sign-request'; +import { Dialog, DialogContent, DialogHeader } from '@kadena/kode-ui'; + +import { useState } from 'react'; +import { Plugin } from '../type'; + +export function SignRequestDialog({ + requestId, + plugin, + onDone, +}: { + requestId: string; + plugin: Plugin; + onDone: () => void; +}) { + const [isOpened, setIsOpened] = useState(true); + const close = () => { + setIsOpened(false); + onDone(); + }; + + return ( + + {`Sign Request from ${plugin.name}`} + + + + + ); +} diff --git a/packages/apps/dev-wallet/src/pages/plugins/plugins.tsx b/packages/apps/dev-wallet/src/pages/plugins/plugins.tsx index 9bfc498815..6145a80857 100644 --- a/packages/apps/dev-wallet/src/pages/plugins/plugins.tsx +++ b/packages/apps/dev-wallet/src/pages/plugins/plugins.tsx @@ -6,16 +6,9 @@ import { SideBarBreadcrumbsItem } from '@kadena/kode-ui/patterns'; import { useEffect, useMemo, useState } from 'react'; import { Link, useSearchParams } from 'react-router-dom'; import { noStyleLinkClass } from '../home/style.css'; +import { PluginCommunicationProvider } from './PluginCommunicationProvider'; import { pluginContainerClass, pluginIconClass } from './style.css'; - -type Plugin = { - id: string; - name: string; - shortName: string; - registry: string; - description: string; - permissions: ['network-list']; -}; +import { Plugin } from './type'; // plugin whitelist const registries = ['/hosted-plugins', 'https://localhost:3000/test-plugins']; @@ -94,37 +87,43 @@ export function Plugins() { if (plugin) { return ( - - } isGlobal> - - plugins - - - {plugin.name} - - - - - -
-
- {getInitials(plugin.shortName).toUpperCase()} -
-
+ + + } isGlobal> + + plugins + + {plugin.name} - -
+ + + + + +
+
+ {getInitials(plugin.shortName).toUpperCase()} +
+
+ {plugin.name} +
+
- {plugin.description} + {plugin.description} +
+ +