-
Notifications
You must be signed in to change notification settings - Fork 3
feat(broswer-api): sign message #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
6ade868
3e08369
c3060f8
e6c9f25
752547b
2a530d4
e1a41dd
8f09c9f
78e0b74
b67f93e
5a9a6e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { Handler } from "@/api/background/utils"; | ||
| import { ApiRequest, ApiResponse, SignMessagePayload } from "@/api/message"; | ||
| import { ApiUtils } from "@/api/background/utils"; | ||
|
|
||
| /** signMessageHandler to serve BrowserMessageType.SIGN_MESSAGE message */ | ||
| export const signMessageHandler: Handler = async ( | ||
| tabId: number, | ||
| message: ApiRequest, | ||
| sendResponse: any, | ||
| ) => { | ||
| if (!message.host) { | ||
| sendResponse(new ApiResponse(message.id, null, "Host is required")); | ||
| return; | ||
| } | ||
|
|
||
| // Check if extension is initialized | ||
| if (!(await ApiUtils.isInitialized())) { | ||
| sendResponse( | ||
| new ApiResponse(message.id, null, "Extension is not initialized"), | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| // Check if host is connected, if not, return error | ||
| if (!(await ApiUtils.isHostConnected(message.host))) { | ||
| sendResponse(new ApiResponse(message.id, null, "Host not connected")); | ||
| return; | ||
| } | ||
|
|
||
| if (!SignMessagePayload.validate(message.payload)) { | ||
| sendResponse(new ApiResponse(message.id, null, "Invalid payload")); | ||
| return; | ||
| } | ||
|
|
||
| // Reconstruct SignMessagePayload from serialized message data to restore methods | ||
| const payload = Object.assign(new SignMessagePayload(""), message.payload); | ||
|
|
||
| const url = browser.runtime.getURL( | ||
| `/popup.html?requestId=${encodeURIComponent(message.id)}&payload=${payload.toUriString()}#/sign-message`, | ||
| ); | ||
|
|
||
| ApiUtils.openPopup(tabId, url); | ||
|
|
||
| const response = await ApiUtils.receiveExtensionMessage(message.id); | ||
| sendResponse(response); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { SignMessagePayload } from "@/api/message"; | ||
| import HotWalletSignMessage from "@/components/screens/browser-api/sign-message/HotWalletSignMessage"; | ||
| import LedgerSignMessage from "@/components/screens/browser-api/sign-message/LedgerSignMessage"; | ||
| import useWalletManager from "@/hooks/useWalletManager.ts"; | ||
| import { useEffect } from "react"; | ||
| import { ApiExtensionUtils } from "@/api/extension"; | ||
| import { ApiResponse } from "@/api/message"; | ||
| import Splash from "@/components/screens/Splash"; | ||
|
|
||
| export default function SignMessageConfirm() { | ||
| const { wallet } = useWalletManager(); | ||
| const requestId = | ||
| new URLSearchParams(window.location.search).get("requestId") ?? ""; | ||
| const encodedPayload = new URLSearchParams(window.location.search).get( | ||
| "payload", | ||
| ); | ||
|
|
||
| const payload = encodedPayload | ||
| ? SignMessagePayload.fromUriString(encodedPayload) | ||
| : null; | ||
|
|
||
| const loading = !wallet || !requestId || !payload; | ||
|
|
||
| useEffect(() => { | ||
| // Handle beforeunload event | ||
| async function beforeunload(event: BeforeUnloadEvent) { | ||
| const denyMessage = new ApiResponse(requestId, false, "User denied"); | ||
| await ApiExtensionUtils.sendMessage(requestId, denyMessage); | ||
| } | ||
|
|
||
| window.addEventListener("beforeunload", beforeunload); | ||
|
|
||
| return () => { | ||
| window.removeEventListener("beforeunload", beforeunload); | ||
| }; | ||
| }, []); | ||
|
|
||
| return ( | ||
| <div className="h-screen p-4"> | ||
| {loading && <Splash />} | ||
| {!loading && wallet.type !== "ledger" && ( | ||
| <HotWalletSignMessage requestId={requestId} payload={payload} /> | ||
| )} | ||
| {!loading && wallet.type === "ledger" && ( | ||
| <LedgerSignMessage requestId={requestId} payload={payload} /> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { SignMessagePayload } from "@/api/message"; | ||
| import SignMessage from "@/components/screens/browser-api/sign-message/SignMessage"; | ||
| import { IWallet } from "@/lib/wallet/wallet-interface.ts"; | ||
| import { AccountFactory } from "@/lib/wallet/wallet-factory"; | ||
| import useWalletManager from "@/hooks/useWalletManager.ts"; | ||
| import useRpcClientStateful from "@/hooks/useRpcClientStateful"; | ||
| import Splash from "@/components/screens/Splash.tsx"; | ||
|
|
||
| type HotWalletSignMessageProps = { | ||
| requestId: string; | ||
| payload: SignMessagePayload; | ||
| }; | ||
|
|
||
| export default function HotWalletSignMessage({ | ||
| requestId, | ||
| payload, | ||
| }: HotWalletSignMessageProps) { | ||
| const { getWalletSecret } = useKeyring(); | ||
| const { wallet: walletInfo, account } = useWalletManager(); | ||
| const { rpcClient, networkId: rpcNetworkId } = useRpcClientStateful(); | ||
| const [wallet, setWallet] = useState<IWallet>(); | ||
| const loading = | ||
| !rpcClient || !wallet || !walletInfo || !account || !rpcNetworkId; | ||
|
|
||
| useEffect(() => { | ||
| if (!rpcClient || !walletInfo || !rpcNetworkId || !account) return; | ||
| if (walletInfo.type !== "mnemonic" && walletInfo.type !== "privateKey") { | ||
| throw new Error("Unsupported wallet type"); | ||
| } | ||
|
|
||
| getWalletSecret({ walletId: walletInfo.id }).then(({ walletSecret }) => { | ||
| const factory = new AccountFactory(rpcClient, rpcNetworkId); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really think we should try to bubble up this wallet instantiation in the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Due to compatibility issues between Ledger and EVM-compatible features, we've decided to postpone this improvement for now. |
||
|
|
||
| switch (walletInfo.type) { | ||
| case "mnemonic": | ||
| setWallet( | ||
| factory.createFromMnemonic(walletSecret.value, account.index), | ||
| ); | ||
| break; | ||
| case "privateKey": | ||
| setWallet(factory.createFromPrivateKey(walletSecret.value)); | ||
| break; | ||
| } | ||
| }); | ||
| }, [rpcClient, walletInfo, account]); | ||
|
|
||
| return ( | ||
| <> | ||
| {loading && <Splash />} | ||
| {!loading && ( | ||
| <SignMessage | ||
| walletSigner={wallet} | ||
| requestId={requestId} | ||
| message={payload.message} | ||
| /> | ||
| )} | ||
| </> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { SignMessagePayload } from "@/api/message"; | ||
| import { AccountFactory } from "@/lib/wallet/wallet-factory"; | ||
| import useRpcClientStateful from "@/hooks/useRpcClientStateful"; | ||
| import { NetworkType } from "@/contexts/SettingsContext"; | ||
| import Splash from "@/components/screens/Splash"; | ||
| import LedgerConnectForSign from "@/components/screens/ledger-connect/LedgerConnectForSign"; | ||
| import SignMessage from "@/components/screens/browser-api/sign-message/SignMessage"; | ||
|
|
||
| type LedgerSignTxProps = { | ||
| requestId: string; | ||
| payload: SignMessagePayload; | ||
| }; | ||
|
|
||
| export default function LedgerSignMessage({ | ||
| requestId, | ||
| payload, | ||
| }: LedgerSignTxProps) { | ||
| const { transport, isAppOpen } = useLedgerTransport(); | ||
| const { rpcClient, networkId } = useRpcClientStateful(); | ||
|
|
||
| const wallet = | ||
| rpcClient && transport | ||
| ? new AccountFactory( | ||
| rpcClient, | ||
| networkId ?? ("mainnet" as NetworkType), | ||
| ).createFromLedger(transport) | ||
| : null; | ||
|
|
||
| return ( | ||
| <> | ||
| {(!transport || !isAppOpen) && ( | ||
| <LedgerConnectForSign showClose={false} showPrevious={false} /> | ||
| )} | ||
| {transport && isAppOpen && !wallet && <Splash />} | ||
| {wallet && isAppOpen && ( | ||
| <SignMessage | ||
| walletSigner={wallet} | ||
| requestId={requestId} | ||
| message={payload.message} | ||
| /> | ||
| )} | ||
| </> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| import useWalletManager from "@/hooks/useWalletManager"; | ||
| import ledgerSignImage from "@/assets/images/ledger-on-sign.svg"; | ||
| import signImage from "@/assets/images/sign.png"; | ||
| import Header from "@/components/GeneralHeader"; | ||
| import { useBoolean } from "usehooks-ts"; | ||
| import { IWallet } from "@/lib/wallet/wallet-interface"; | ||
| import { ApiExtensionUtils } from "@/api/extension"; | ||
| import { ApiResponse } from "@/api/message"; | ||
|
|
||
| type SignMessageProps = { | ||
| requestId: string; | ||
| walletSigner: IWallet; | ||
| message: string; | ||
| }; | ||
|
|
||
| export default function SignMessage({ | ||
| requestId, | ||
| walletSigner, | ||
| message, | ||
| }: SignMessageProps) { | ||
| const { wallet } = useWalletManager(); | ||
| const { value: isSigning, toggle: toggleIsSigning } = useBoolean(false); | ||
|
|
||
| const onConfirm = async () => { | ||
| if (isSigning) { | ||
| return; | ||
| } | ||
|
|
||
| toggleIsSigning(); | ||
| try { | ||
| // Sign the message | ||
| const signed = await walletSigner.signMessage(message); | ||
| await ApiExtensionUtils.sendMessage( | ||
| requestId, | ||
| new ApiResponse(requestId, signed), | ||
| ); | ||
| toggleIsSigning(); | ||
| } catch (err) { | ||
| await ApiExtensionUtils.sendMessage( | ||
| requestId, | ||
| new ApiResponse( | ||
| requestId, | ||
| null, | ||
| "Failed to sign message: " + (err as any).toString(), | ||
| ), | ||
| ); | ||
| } finally { | ||
| window.close(); | ||
| } | ||
| }; | ||
|
|
||
| const cancel = async () => { | ||
| await ApiExtensionUtils.sendMessage( | ||
| requestId, | ||
| new ApiResponse(requestId, null, "User cancelled"), | ||
| ); | ||
| window.close(); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="flex h-full flex-col justify-between"> | ||
| <div> | ||
| <Header showPrevious={false} showClose={false} title="Confirm" /> | ||
| <div className="relative"> | ||
| {wallet?.type !== "ledger" && ( | ||
| <img src={signImage} alt="Sign" className="mx-auto" /> | ||
| )} | ||
| {wallet?.type === "ledger" && ( | ||
| <img src={ledgerSignImage} alt="Sign" className="mx-auto" /> | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Confirm Content */} | ||
| <div className="text-center"> | ||
| <h2 className="mt-4 text-2xl font-semibold">Sign Message</h2> | ||
| <p className="mt-2 text-base text-daintree-400"> | ||
| Please confirm the message you are signing | ||
| </p> | ||
| <div className="mt-4 break-words rounded-md bg-daintree-700 p-4"> | ||
| <p className="text-start text-sm">{message}</p> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Buttons */} | ||
| <div className="flex gap-2 text-base font-semibold"> | ||
| <button className="rounded-full p-5 text-[#7B9AAA]" onClick={cancel}> | ||
| Cancel | ||
| </button> | ||
| <button | ||
| className="flex flex-auto items-center justify-center rounded-full bg-icy-blue-400 py-5 font-semibold hover:bg-icy-blue-600" | ||
| onClick={onConfirm} | ||
| > | ||
| {isSigning ? ( | ||
| <div className="flex gap-2"> | ||
| <div | ||
| className="inline-block size-5 animate-spin self-center rounded-full border-[3px] border-current border-t-[#A2F5FF] text-icy-blue-600" | ||
| role="status" | ||
| aria-label="loading" | ||
| /> | ||
| {wallet?.type === "ledger" && ( | ||
| <span className="text-sm">Please approve on Ledger</span> | ||
| )} | ||
| </div> | ||
| ) : ( | ||
| `Confirm` | ||
| )} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is
accountmissing here? can do!loading?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you are mentioning
wallet.It is for setting
IWallet, so unluckily, so we can't check wallet here, or the wallet would never be initialized.