diff --git a/api/background/background-service.ts b/api/background/background-service.ts index 4408e89e..fdda0970 100644 --- a/api/background/background-service.ts +++ b/api/background/background-service.ts @@ -1,7 +1,7 @@ -import { connectHandler } from "@/api/background/handlers/connect"; -import { getAccountHandler } from "@/api/background/handlers/getAccount"; -import { signAndBroadcastTxHandler } from "@/api/background/handlers/signAndBroadcastTx"; -import { signTxHandler } from "@/api/background/handlers/signTx"; +import { connectHandler } from "@/api/background/handlers/kaspa/connect"; +import { getAccountHandler } from "@/api/background/handlers/kaspa/getAccount"; +import { signAndBroadcastTxHandler } from "@/api/background/handlers/kaspa/signAndBroadcastTx"; +import { signTxHandler } from "@/api/background/handlers/kaspa/signTx"; import { Action, ApiRequestWithHostSchema, @@ -9,6 +9,7 @@ import { } from "@/api/message"; import { getNetwork } from "@/api/background/handlers/get-network.ts"; import { ethereumRequestHandler } from "@/api/background/handlers/ethereum/request"; +import { signMessageHandler } from "@/api/background/handlers/kaspa/signMessage"; export class BackgroundService { public listen(): void { @@ -59,6 +60,7 @@ export class BackgroundService { [Action.SIGN_TX]: signTxHandler, [Action.GET_NETWORK]: getNetwork, [Action.ETHEREUM_REQUEST]: ethereumRequestHandler, + [Action.SIGN_MESSAGE]: signMessageHandler, }; return handlers[action]; diff --git a/api/background/handlers/ethereum/sendTransaction.ts b/api/background/handlers/ethereum/sendTransaction.ts index ff1e2bb3..302e1692 100644 --- a/api/background/handlers/ethereum/sendTransaction.ts +++ b/api/background/handlers/ethereum/sendTransaction.ts @@ -3,10 +3,28 @@ import { RpcError, RPC_ERRORS, RpcRequestSchema, - ethereumTransactionRequestSchema, } from "@/api/message"; import { ApiUtils } from "@/api/background/utils"; import { isMatchCurrentAddress, isUserDeniedResponse } from "./utils"; +import { isAddress, isHex } from "viem"; +import { z } from "zod"; + +export const ethereumTransactionRequestSchema = z.object({ + from: z.string().refine(isAddress, "Must be a valid Ethereum address"), + to: z.string().refine(isAddress, "Must be a valid Ethereum address"), + value: z.string().refine(isHex, "Value must be a hex string").optional(), + data: z.string().refine(isHex, "Data must be a hex string").optional(), + maxFeePerGas: z + .string() + .refine(isHex, "Max fee per gas must be a hex string") + .optional(), + maxPriorityFeePerGas: z + .string() + .refine(isHex, "Max priority fee must be a hex string") + .optional(), +}); + +// ================================================================================ export const sendTransactionHandler = async ( tabId: number, diff --git a/api/background/handlers/connect.ts b/api/background/handlers/kaspa/connect.ts similarity index 82% rename from api/background/handlers/connect.ts rename to api/background/handlers/kaspa/connect.ts index fb95e2cf..f4f419f6 100644 --- a/api/background/handlers/connect.ts +++ b/api/background/handlers/kaspa/connect.ts @@ -1,5 +1,15 @@ -import { ApiRequestWithHost, ConnectPayloadSchema } from "@/api/message"; +import { ApiRequestWithHost } from "@/api/message"; import { ApiUtils, Handler } from "@/api/background/utils"; +import { NetworkType } from "@/contexts/SettingsContext"; +import { z } from "zod"; + +export const ConnectPayloadSchema = z.object({ + networkId: z.nativeEnum(NetworkType), + name: z.string(), + icon: z.string().optional(), +}); + +export type ConnectPayload = z.infer; /** Connect handler to serve BrowserMessageType.CONNECT message */ export const connectHandler: Handler = async ( diff --git a/api/background/handlers/getAccount.ts b/api/background/handlers/kaspa/getAccount.ts similarity index 100% rename from api/background/handlers/getAccount.ts rename to api/background/handlers/kaspa/getAccount.ts diff --git a/api/background/handlers/signAndBroadcastTx.ts b/api/background/handlers/kaspa/signAndBroadcastTx.ts similarity index 93% rename from api/background/handlers/signAndBroadcastTx.ts rename to api/background/handlers/kaspa/signAndBroadcastTx.ts index e5650cbd..5db72b05 100644 --- a/api/background/handlers/signAndBroadcastTx.ts +++ b/api/background/handlers/kaspa/signAndBroadcastTx.ts @@ -1,6 +1,8 @@ import { Handler } from "@/api/background/utils"; -import { ApiRequestWithHost, SignTxPayloadSchema } from "@/api/message"; +import { ApiRequestWithHost } from "@/api/message"; import { ApiUtils } from "@/api/background/utils"; +import { SignTxPayloadSchema } from "./utils"; + /** signAndBroadcastTx handler to serve BrowserMessageType.SIGN_AND_BROADCAST_TX message */ export const signAndBroadcastTxHandler: Handler = async ( tabId: number, diff --git a/api/background/handlers/kaspa/signMessage.ts b/api/background/handlers/kaspa/signMessage.ts new file mode 100644 index 00000000..019fc282 --- /dev/null +++ b/api/background/handlers/kaspa/signMessage.ts @@ -0,0 +1,64 @@ +import { Handler } from "@/api/background/utils"; +import { ApiRequestWithHost, ApiResponse } from "@/api/message"; +import { ApiUtils } from "@/api/background/utils"; +import { z } from "zod"; + +export const SignMessagePayloadSchema = z.object({ + message: z.string(), +}); + +export type SignMessagePayload = z.infer; + +// ================================================================================ + +/** signMessageHandler to serve BrowserMessageType.SIGN_MESSAGE message */ +export const signMessageHandler: Handler = async ( + tabId: number, + message: ApiRequestWithHost, + sendResponse: any, +) => { + if (!message.host) { + sendResponse( + ApiUtils.createApiResponse(message.id, null, "Host is required"), + ); + return; + } + + // Check if extension is initialized + if (!(await ApiUtils.isInitialized())) { + sendResponse( + ApiUtils.createApiResponse( + message.id, + null, + "Extension is not initialized", + ), + ); + return; + } + + // Check if host is connected, if not, return error + if (!(await ApiUtils.isHostConnected(message.host))) { + sendResponse( + ApiUtils.createApiResponse(message.id, null, "Host not connected"), + ); + return; + } + + const result = SignMessagePayloadSchema.safeParse(message.payload); + if (!result.success) { + sendResponse( + ApiUtils.createApiResponse(message.id, null, "Invalid transaction data"), + ); + return; + } + + const url = new URL(browser.runtime.getURL("/popup.html")); + url.hash = "/sign-message"; + url.searchParams.set("requestId", message.id); + url.searchParams.set("payload", JSON.stringify(result.data)); + + ApiUtils.openPopup(tabId, url.toString()); + + const response = await ApiUtils.receiveExtensionMessage(message.id); + sendResponse(response); +}; diff --git a/api/background/handlers/signTx.ts b/api/background/handlers/kaspa/signTx.ts similarity index 93% rename from api/background/handlers/signTx.ts rename to api/background/handlers/kaspa/signTx.ts index c47abe73..a4fcb246 100644 --- a/api/background/handlers/signTx.ts +++ b/api/background/handlers/kaspa/signTx.ts @@ -1,10 +1,7 @@ import { Handler } from "@/api/background/utils"; -import { - ApiRequestWithHost, - ApiResponse, - SignTxPayloadSchema, -} from "@/api/message"; +import { ApiRequestWithHost } from "@/api/message"; import { ApiUtils } from "@/api/background/utils"; +import { SignTxPayloadSchema } from "./utils"; /** signTxHandler to serve BrowserMessageType.SIGN_TX message */ export const signTxHandler: Handler = async ( diff --git a/api/background/handlers/kaspa/utils.ts b/api/background/handlers/kaspa/utils.ts new file mode 100644 index 00000000..bf43f8c8 --- /dev/null +++ b/api/background/handlers/kaspa/utils.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; +import { ScriptOption } from "@/lib/wallet/wallet-interface.ts"; + +export const SignTxPayloadSchema = z.object({ + networkId: z.string(), + txJson: z.string(), + scripts: z.array(z.custom()).optional(), +}); + +export type SignTxPayload = z.infer; diff --git a/api/browser.ts b/api/browser.ts index 87b98edd..fe35b512 100644 --- a/api/browser.ts +++ b/api/browser.ts @@ -1,14 +1,10 @@ import { v4 as uuid } from "uuid"; -import { - Action, - ApiRequest, - ApiResponseSchema, - ConnectPayloadSchema, - SignTxPayloadSchema, -} from "@/api/message"; +import { Action, ApiRequest, ApiResponseSchema } from "@/api/message"; import { ScriptOption } from "@/lib/wallet/wallet-interface.ts"; import { EthereumBrowserAPI } from "./ethereum"; -import { ApiUtils } from "@/api/background/utils.ts"; +import { ConnectPayloadSchema } from "@/api/background/handlers/kaspa/connect"; +import { SignTxPayloadSchema } from "@/api/background/handlers/kaspa/utils"; +import { SignMessagePayloadSchema } from "@/api/background/handlers/kaspa/signMessage"; function createApiRequest( action: Action, @@ -27,12 +23,7 @@ function createApiRequest( export class KastleBrowserAPI { public readonly ethereum = new EthereumBrowserAPI(); - constructor() { - window.postMessage( - ApiUtils.createApiResponse("kastle_installed", []), - window.location.origin, - ); - } + constructor() {} async connect( networkId: "mainnet" | "testnet-10" = "mainnet", @@ -132,6 +123,20 @@ export class KastleBrowserAPI { return await this.receiveMessageWithTimeout(requestId); } + async signMessage(message: string): Promise { + const requestId = uuid(); + const request = createApiRequest( + Action.SIGN_MESSAGE, + requestId, + SignMessagePayloadSchema.parse({ + message, + }), + ); + window.postMessage(request, "*"); + + return await this.receiveMessageWithTimeout(requestId); + } + private createReceiveCallback(id: string) { return (event: MessageEvent) => { if (event.origin !== window.location.origin) { diff --git a/api/message.ts b/api/message.ts index ea8220ae..ecf3b14b 100644 --- a/api/message.ts +++ b/api/message.ts @@ -1,7 +1,4 @@ -import { ScriptOption } from "@/lib/wallet/wallet-interface.ts"; import { z } from "zod"; -import { NetworkType } from "@/contexts/SettingsContext.tsx"; -import { isAddress, isHex } from "viem"; export enum Action { CONNECT, @@ -10,16 +7,9 @@ export enum Action { SIGN_TX, GET_NETWORK, ETHEREUM_REQUEST, + SIGN_MESSAGE, } -export const SignTxPayloadSchema = z.object({ - networkId: z.string(), - txJson: z.string(), - scripts: z.array(z.custom()).optional(), -}); - -export type SignTxPayload = z.infer; - // ================================================================================================ export const RpcRequestSchema = z.object({ @@ -83,31 +73,6 @@ export enum ETHEREUM_METHODS { WALLET_SWITCH_ETHEREUM_NETWORK = "wallet_switchEthereumChain", } -export const ethereumTransactionRequestSchema = z.object({ - from: z.string().refine(isAddress, "Must be a valid Ethereum address"), - to: z.string().refine(isAddress, "Must be a valid Ethereum address"), - value: z.string().refine(isHex, "Value must be a hex string").optional(), - data: z.string().refine(isHex, "Data must be a hex string").optional(), - maxFeePerGas: z - .string() - .refine(isHex, "Max fee per gas must be a hex string") - .optional(), - maxPriorityFeePerGas: z - .string() - .refine(isHex, "Max priority fee must be a hex string") - .optional(), -}); - -// ================================================================================================ - -export const ConnectPayloadSchema = z.object({ - networkId: z.nativeEnum(NetworkType), - name: z.string(), - icon: z.string().optional(), -}); - -export type ConnectPayload = z.infer; - // ================================================================================================ export const ApiRequestSchema = z.object({ diff --git a/components/screens/browser-api/ConnectConfirm.tsx b/components/screens/browser-api/ConnectConfirm.tsx index ab8e3989..747e9bdc 100644 --- a/components/screens/browser-api/ConnectConfirm.tsx +++ b/components/screens/browser-api/ConnectConfirm.tsx @@ -1,6 +1,5 @@ import { ApiExtensionUtils } from "@/api/extension"; import { useSettings } from "@/hooks/useSettings"; -import { useEffect } from "react"; import { NetworkType } from "@/contexts/SettingsContext.tsx"; import Header from "@/components/GeneralHeader"; import Link from "@/assets/images/link.svg"; @@ -23,7 +22,7 @@ export default function ConnectConfirm() { const urlSearchParams = new URLSearchParams(window.location.search); const requestId = urlSearchParams.get("requestId") ?? ""; const host = urlSearchParams.get("host") ?? ""; - const network = urlSearchParams.get("network") ?? ""; + const network = urlSearchParams.get("network") ?? settings?.networkId; const tabName = urlSearchParams.get("name") ?? "Unknown"; const icon = urlSearchParams.get("icon") ?? undefined; @@ -47,7 +46,8 @@ export default function ConnectConfirm() { } const walletConnections = settings.walletConnections ?? {}; - const targetNetwork = (network as NetworkType) ?? settings.networkId; + const targetNetwork = (network ?? settings.networkId) as NetworkType; + const connections = walletConnections[selectedWalletId]?.[selectedAccountIndex]?.[ targetNetwork @@ -110,6 +110,7 @@ export default function ConnectConfirm() { background: "bg-yellow-800", }, ]; + const selectedNetwork = networks.find( (n) => n.id === (network ?? settings?.networkId), ); diff --git a/components/screens/browser-api/SignAndBroadcastTxConfirm.tsx b/components/screens/browser-api/SignAndBroadcastTxConfirm.tsx index 2e9ec98d..61285886 100644 --- a/components/screens/browser-api/SignAndBroadcastTxConfirm.tsx +++ b/components/screens/browser-api/SignAndBroadcastTxConfirm.tsx @@ -1,4 +1,4 @@ -import { SignTxPayloadSchema } from "@/api/message"; +import { SignTxPayloadSchema } from "@/api/background/handlers/kaspa/utils"; import HotWalletSignAndBroadcast from "@/components/screens/browser-api/sign-and-broadcast/HotWalletSignAndBroadcast"; import LedgerSignAndBroadcast from "@/components/screens/browser-api/sign-and-broadcast/LedgerSignAndBroadcast"; import useWalletManager from "@/hooks/useWalletManager.ts"; diff --git a/components/screens/browser-api/SignMessageConfirm.tsx b/components/screens/browser-api/SignMessageConfirm.tsx new file mode 100644 index 00000000..0f8ea06f --- /dev/null +++ b/components/screens/browser-api/SignMessageConfirm.tsx @@ -0,0 +1,42 @@ +import { SignMessagePayloadSchema } from "@/api/background/handlers/kaspa/signMessage"; +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 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 + ? JSON.parse(decodeURIComponent(encodedPayload)) + : null; + + const parsedPayload = payload + ? SignMessagePayloadSchema.parse(payload) + : null; + + const loading = !wallet || !requestId || !parsedPayload; + + return ( +
+ {loading && } + {!loading && wallet.type !== "ledger" && ( + + )} + {!loading && wallet.type === "ledger" && ( + + )} +
+ ); +} diff --git a/components/screens/browser-api/SignTxConfirm.tsx b/components/screens/browser-api/SignTxConfirm.tsx index ee18b4b9..843efebb 100644 --- a/components/screens/browser-api/SignTxConfirm.tsx +++ b/components/screens/browser-api/SignTxConfirm.tsx @@ -1,4 +1,4 @@ -import { SignTxPayloadSchema } from "@/api/message"; +import { SignTxPayloadSchema } from "@/api/background/handlers/kaspa/utils"; import HotWalletSignTx from "@/components/screens/browser-api/sign-tx/HotWalletSignTx"; import LedgerSignTx from "@/components/screens/browser-api/sign-tx/LedgerSignTx"; import useWalletManager from "@/hooks/useWalletManager.ts"; diff --git a/components/screens/browser-api/ethereum/EthereumSignMessageConfirm.tsx b/components/screens/browser-api/ethereum/EthereumSignMessageConfirm.tsx index 54da8917..2702414e 100644 --- a/components/screens/browser-api/ethereum/EthereumSignMessageConfirm.tsx +++ b/components/screens/browser-api/ethereum/EthereumSignMessageConfirm.tsx @@ -1,10 +1,6 @@ import HotWalletSignMessage from "@/components/screens/browser-api/ethereum/sign-message/HotWalletSignMessage"; import useWalletManager from "@/hooks/useWalletManager.ts"; -import { useEffect } from "react"; -import { ApiExtensionUtils } from "@/api/extension"; -import { RPC_ERRORS } from "@/api/message"; import Splash from "@/components/screens/Splash"; -import { ApiUtils } from "@/api/background/utils"; export default function EthereumSignMessageConfirm() { const { wallet } = useWalletManager(); diff --git a/components/screens/browser-api/ethereum/send-transaction/SendTransaction.tsx b/components/screens/browser-api/ethereum/send-transaction/SendTransaction.tsx index 7cbce3b4..bebb12a4 100644 --- a/components/screens/browser-api/ethereum/send-transaction/SendTransaction.tsx +++ b/components/screens/browser-api/ethereum/send-transaction/SendTransaction.tsx @@ -6,7 +6,7 @@ import Header from "@/components/GeneralHeader"; import { useBoolean } from "usehooks-ts"; import { ApiExtensionUtils } from "@/api/extension"; import { ApiUtils } from "@/api/background/utils"; -import { RPC_ERRORS, ethereumTransactionRequestSchema } from "@/api/message"; +import { RPC_ERRORS } from "@/api/message"; import { TransactionSerializable, hexToBigInt, @@ -15,6 +15,7 @@ import { } from "viem"; import { kairos } from "viem/chains"; import { estimateFeesPerGas } from "viem/actions"; +import { ethereumTransactionRequestSchema } from "@/api/background/handlers/ethereum/sendTransaction"; type SignTransactionProps = { walletSigner: IWallet; diff --git a/components/screens/browser-api/sign-and-broadcast/HotWalletSignAndBroadcast.tsx b/components/screens/browser-api/sign-and-broadcast/HotWalletSignAndBroadcast.tsx index 121ee1ae..c0446dbd 100644 --- a/components/screens/browser-api/sign-and-broadcast/HotWalletSignAndBroadcast.tsx +++ b/components/screens/browser-api/sign-and-broadcast/HotWalletSignAndBroadcast.tsx @@ -1,4 +1,4 @@ -import { SignTxPayload } from "@/api/message"; +import { SignTxPayload } from "@/api/background/handlers/kaspa/utils"; import SignAndBroadcast from "@/components/screens/browser-api/sign-and-broadcast/SignAndBroadcast"; import { IWallet } from "@/lib/wallet/wallet-interface.ts"; import { AccountFactory } from "@/lib/wallet/wallet-factory"; diff --git a/components/screens/browser-api/sign-and-broadcast/LedgerSignAndBroadcast.tsx b/components/screens/browser-api/sign-and-broadcast/LedgerSignAndBroadcast.tsx index dca93a5a..c4f4742e 100644 --- a/components/screens/browser-api/sign-and-broadcast/LedgerSignAndBroadcast.tsx +++ b/components/screens/browser-api/sign-and-broadcast/LedgerSignAndBroadcast.tsx @@ -1,4 +1,4 @@ -import { SignTxPayload } from "@/api/message"; +import { SignTxPayload } from "@/api/background/handlers/kaspa/utils"; import LedgerNotSupported from "@/components/screens/browser-api/sign/LedgerNotSupported"; import SignAndBroadcast from "@/components/screens/browser-api/sign-and-broadcast/SignAndBroadcast"; import { AccountFactory } from "@/lib/wallet/wallet-factory"; diff --git a/components/screens/browser-api/sign-and-broadcast/SignAndBroadcast.tsx b/components/screens/browser-api/sign-and-broadcast/SignAndBroadcast.tsx index e8a1f7b4..390468be 100644 --- a/components/screens/browser-api/sign-and-broadcast/SignAndBroadcast.tsx +++ b/components/screens/browser-api/sign-and-broadcast/SignAndBroadcast.tsx @@ -1,4 +1,4 @@ -import { SignTxPayload } from "@/api/message"; +import { SignTxPayload } from "@/api/background/handlers/kaspa/utils"; import { ApiExtensionUtils } from "@/api/extension"; import { IWallet } from "@/lib/wallet/wallet-interface.ts"; import useWalletManager from "@/hooks/useWalletManager"; diff --git a/components/screens/browser-api/sign-message/HotWalletSignMessage.tsx b/components/screens/browser-api/sign-message/HotWalletSignMessage.tsx new file mode 100644 index 00000000..df10b767 --- /dev/null +++ b/components/screens/browser-api/sign-message/HotWalletSignMessage.tsx @@ -0,0 +1,59 @@ +import { SignMessagePayload } from "@/api/background/handlers/kaspa/signMessage"; +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(); + 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); + + 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 && } + {!loading && ( + + )} + + ); +} diff --git a/components/screens/browser-api/sign-message/LedgerSignMessage.tsx b/components/screens/browser-api/sign-message/LedgerSignMessage.tsx new file mode 100644 index 00000000..2dbd59da --- /dev/null +++ b/components/screens/browser-api/sign-message/LedgerSignMessage.tsx @@ -0,0 +1,44 @@ +import { SignMessagePayload } from "@/api/background/handlers/kaspa/signMessage"; +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) && ( + + )} + {transport && isAppOpen && !wallet && } + {wallet && isAppOpen && ( + + )} + + ); +} diff --git a/components/screens/browser-api/sign-message/SignMessage.tsx b/components/screens/browser-api/sign-message/SignMessage.tsx new file mode 100644 index 00000000..500453c6 --- /dev/null +++ b/components/screens/browser-api/sign-message/SignMessage.tsx @@ -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 { ApiUtils } from "@/api/background/utils"; + +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, + ApiUtils.createApiResponse(requestId, signed), + ); + toggleIsSigning(); + } catch (err) { + await ApiExtensionUtils.sendMessage( + requestId, + ApiUtils.createApiResponse( + requestId, + null, + "Failed to sign message: " + (err as any).toString(), + ), + ); + } finally { + window.close(); + } + }; + + const cancel = async () => { + await ApiExtensionUtils.sendMessage( + requestId, + ApiUtils.createApiResponse(requestId, null, "User cancelled"), + ); + window.close(); + }; + + return ( +
+
+
+
+ {wallet?.type !== "ledger" && ( + Sign + )} + {wallet?.type === "ledger" && ( + Sign + )} +
+ + {/* Confirm Content */} +
+

Sign Message

+

+ Please confirm the message you are signing +

+
+

{message}

+
+
+
+ + {/* Buttons */} +
+ + +
+
+ ); +} diff --git a/components/screens/browser-api/sign-tx/HotWalletSignTx.tsx b/components/screens/browser-api/sign-tx/HotWalletSignTx.tsx index 1ad78035..34286072 100644 --- a/components/screens/browser-api/sign-tx/HotWalletSignTx.tsx +++ b/components/screens/browser-api/sign-tx/HotWalletSignTx.tsx @@ -1,4 +1,4 @@ -import { SignTxPayload } from "@/api/message"; +import { SignTxPayload } from "@/api/background/handlers/kaspa/utils"; import SignTx from "@/components/screens/browser-api/sign-tx/SignTx"; import { IWallet } from "@/lib/wallet/wallet-interface.ts"; import { AccountFactory } from "@/lib/wallet/wallet-factory"; diff --git a/components/screens/browser-api/sign-tx/LedgerSignTx.tsx b/components/screens/browser-api/sign-tx/LedgerSignTx.tsx index 6a517572..5a8ce990 100644 --- a/components/screens/browser-api/sign-tx/LedgerSignTx.tsx +++ b/components/screens/browser-api/sign-tx/LedgerSignTx.tsx @@ -1,4 +1,4 @@ -import { SignTxPayload } from "@/api/message"; +import { SignTxPayload } from "@/api/background/handlers/kaspa/utils"; import LedgerNotSupported from "@/components/screens/browser-api/sign/LedgerNotSupported"; import SignTx from "@/components/screens/browser-api/sign-tx/SignTx"; import { AccountFactory } from "@/lib/wallet/wallet-factory"; diff --git a/components/screens/browser-api/sign-tx/SignTx.tsx b/components/screens/browser-api/sign-tx/SignTx.tsx index 2b3b38a3..9867e172 100644 --- a/components/screens/browser-api/sign-tx/SignTx.tsx +++ b/components/screens/browser-api/sign-tx/SignTx.tsx @@ -1,4 +1,4 @@ -import { ApiResponse, SignTxPayload } from "@/api/message"; +import { SignTxPayload } from "@/api/background/handlers/kaspa/utils"; import { ApiExtensionUtils } from "@/api/extension"; import { IWallet } from "@/lib/wallet/wallet-interface.ts"; import useRpcClientStateful from "@/hooks/useRpcClientStateful"; @@ -33,8 +33,7 @@ export default function SignTx({ wallet, requestId, payload }: SignTxProps) { ApiUtils.createApiResponse( requestId, null, - "Failed to sign and broadcast transaction: " + - (err as any).toString(), + "Failed to sign transaction: " + (err as any).toString(), ), ); } finally { diff --git a/components/screens/browser-api/sign/DetailsSelector.tsx b/components/screens/browser-api/sign/DetailsSelector.tsx index 604faa73..57abf577 100644 --- a/components/screens/browser-api/sign/DetailsSelector.tsx +++ b/components/screens/browser-api/sign/DetailsSelector.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { SignTxPayload } from "@/api/message"; +import { SignTxPayload } from "@/api/background/handlers/kaspa/utils"; import TransactionDetailsBox from "@/components/screens/browser-api/sign/TransactionBox"; import ScriptItem from "@/components/screens/browser-api/sign/ScriptItem"; import { twMerge } from "tailwind-merge"; diff --git a/components/screens/browser-api/sign/SignConfirm.tsx b/components/screens/browser-api/sign/SignConfirm.tsx index 1e9d2aad..e5a7e796 100644 --- a/components/screens/browser-api/sign/SignConfirm.tsx +++ b/components/screens/browser-api/sign/SignConfirm.tsx @@ -1,4 +1,4 @@ -import { SignTxPayload } from "@/api/message"; +import { SignTxPayload } from "@/api/background/handlers/kaspa/utils"; import { NetworkType } from "@/contexts/SettingsContext.tsx"; import useWalletManager from "@/hooks/useWalletManager"; import { diff --git a/docs/index.html b/docs/index.html index 23bb01fe..180c265b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -41,6 +41,12 @@

Kastle Basic

Error: None
+
+ +
Signature:
+
Error: None
+
+

KRC20

diff --git a/docs/index.js b/docs/index.js index 84248696..712d7382 100644 --- a/docs/index.js +++ b/docs/index.js @@ -87,6 +87,17 @@ document } }); +document.getElementById("signMessage").addEventListener("click", async () => { + try { + const message = "Hello, World!"; + const signature = await kastle.signMessage(message); + document.getElementById("signature").innerText = signature; + document.getElementById("signMessageError").innerText = "None"; + } catch (error) { + document.getElementById("signMessageError").innerText = error.message; + } +}); + function createKRC20ScriptBuilder(data) { const { Opcodes } = kaspaWasm; const accountPublicKey = document.getElementById("publicKey").innerText; diff --git a/entrypoints/content.ts b/entrypoints/content.ts index fdac4010..2c184311 100644 --- a/entrypoints/content.ts +++ b/entrypoints/content.ts @@ -2,6 +2,7 @@ import { ApiRequestSchema, ApiRequestWithHostSchema } from "@/api/message"; import { EthereumAccountsChangedListener } from "@/api/content-script/listeners/ethereum/accountsChanged"; import { watchSettingsUpdated } from "@/api/content-script/listeners/settings-updated.ts"; import { watchWalletSettingsUpdated } from "@/api/content-script/listeners/wallet-settings-updated.ts"; +import { ApiUtils } from "@/api/background/utils"; export default defineContentScript({ matches: ["*://*/*"], @@ -11,6 +12,12 @@ export default defineContentScript({ keepInDom: true, }); + // Emit the kastle_installed event to the page to notify that the extension is installed + window.postMessage( + ApiUtils.createApiResponse("kastle_installed", []), + window.location.origin, + ); + // TODO: implement tabs connections manager and authentication for listeners new EthereumAccountsChangedListener().start(); diff --git a/entrypoints/popup/router.tsx b/entrypoints/popup/router.tsx index 3d57b491..252fc7fc 100644 --- a/entrypoints/popup/router.tsx +++ b/entrypoints/popup/router.tsx @@ -60,6 +60,7 @@ import EthereumSignMessageConfirm from "@/components/screens/browser-api/ethereu import EthereumSendTransactionConfirm from "@/components/screens/browser-api/ethereum/EthereumSendTransactionConfirm"; import BrowserAPILayout from "@/components/layouts/BrowserAPILayout"; import Unlocked from "@/components/screens/browser-api/Unlocked"; +import SignMessageConfirm from "@/components/screens/browser-api/SignMessageConfirm"; const loadKaspaWasm = async () => { await init(kaspaModule); @@ -239,6 +240,12 @@ export const router = createHashRouter([ element: , loader: browserAPIKeyringGuard, children: [ + { + path: "unlocked", + element: , + }, + + // Kaspa BrowserAPI routes { path: "connect", element: }, { path: "sign-and-broadcast-tx", @@ -249,8 +256,8 @@ export const router = createHashRouter([ element: , }, { - path: "unlocked", - element: , + path: "sign-message", + element: , }, // Ethereum BrowserAPI routes diff --git a/lib/wallet/account/hot-wallet-account.ts b/lib/wallet/account/hot-wallet-account.ts index a2cb839f..ad7ae251 100644 --- a/lib/wallet/account/hot-wallet-account.ts +++ b/lib/wallet/account/hot-wallet-account.ts @@ -15,6 +15,7 @@ import { Transaction, UtxoEntryReference, XPrv, + signMessage, } from "@/wasm/core/kaspa"; import { @@ -211,6 +212,10 @@ export class HotWalletAccount implements IWallet { return this.getPrivateKey().toPublicKey(); } + signMessage(message: string): string { + return signMessage({ message, privateKey: this.getPrivateKey() }); + } + private async commitScript(p2SHAddress: string) { const publicKey = this.getPublicKey(); const address = publicKey.toAddress(this.networkId); diff --git a/lib/wallet/account/hot-wallet-private-key.ts b/lib/wallet/account/hot-wallet-private-key.ts index 657fd116..09d80e30 100644 --- a/lib/wallet/account/hot-wallet-private-key.ts +++ b/lib/wallet/account/hot-wallet-private-key.ts @@ -14,6 +14,7 @@ import { signTransaction, Transaction, UtxoEntryReference, + signMessage, } from "@/wasm/core/kaspa"; import { @@ -188,6 +189,10 @@ export class HotWalletPrivateKey implements IWallet { scriptBuilder.encodePayToScriptHashSignatureScript(signature); } + signMessage(message: string): string { + return signMessage({ message, privateKey: this.privateKey }); + } + private async commitScript(p2SHAddress: string) { const publicKey = this.getPublicKey(); const address = publicKey.toAddress(this.networkId); diff --git a/lib/wallet/account/ledger-account.ts b/lib/wallet/account/ledger-account.ts index 9db43c69..63bf172d 100644 --- a/lib/wallet/account/ledger-account.ts +++ b/lib/wallet/account/ledger-account.ts @@ -198,6 +198,17 @@ export class LedgerAccount implements IWallet { return this.toRpcTransaction(ledgerTx); } + async signMessage(message: string): Promise { + return ( + await this.app.signMessage( + message, + 0, + 0, + this.accountIndex + LEDGER_ACCOUNT_INDEX_OFFSET, + ) + ).signature; + } + private async getUtxos(): Promise { const address = await this.getAddress(); return ( diff --git a/lib/wallet/wallet-interface.ts b/lib/wallet/wallet-interface.ts index c0e71231..ffd10d87 100644 --- a/lib/wallet/wallet-interface.ts +++ b/lib/wallet/wallet-interface.ts @@ -46,6 +46,8 @@ export interface IWallet { signTx(tx: Transaction, scripts?: ScriptOption[]): Promise; + signMessage(message: string): string | Promise; + performCommitReveal( scriptBuilder: ScriptBuilder, revealPriorityFee: string, // KAS