diff --git a/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx b/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx index c5186cd0f..a3c98dac6 100644 --- a/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx +++ b/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx @@ -1,6 +1,12 @@ import { SigningType } from "@airgap/beacon-wallet"; +import { useToast } from "@chakra-ui/react"; import { useDynamicModalContext } from "@umami/components"; -import { type ImplicitAccount, estimate, toAccountOperations } from "@umami/core"; +import { + type ImplicitAccount, + estimate, + getPublicKeyAndCurve, + toAccountOperations, +} from "@umami/core"; import { useAsyncActionHandler, useFindNetwork, @@ -9,7 +15,7 @@ import { walletKit, } from "@umami/state"; import { WalletConnectError } from "@umami/utils"; -import { formatJsonRpcError } from "@walletconnect/jsonrpc-utils"; +import { formatJsonRpcError, formatJsonRpcResult } from "@walletconnect/jsonrpc-utils"; import { type SessionTypes, type SignClientTypes, type Verify } from "@walletconnect/types"; import { type SdkErrorKey, getSdkError } from "@walletconnect/utils"; @@ -21,7 +27,6 @@ import { type SignHeaderProps, type SignPayloadProps, } from "../SendFlow/utils"; - /** * @returns a function that handles a beacon message and opens a modal with the appropriate content * @@ -34,6 +39,7 @@ export const useHandleWcRequest = () => { const getAccount = useGetOwnedAccountSafe(); const getImplicitAccount = useGetImplicitAccount(); const findNetwork = useFindNetwork(); + const toast = useToast(); return async ( event: { @@ -57,11 +63,41 @@ export const useHandleWcRequest = () => { switch (request.method) { case "tezos_getAccounts": { - throw new WalletConnectError( - "Getting accounts is not supported yet", - "WC_METHOD_UNSUPPORTED", - session + const wcPeers = walletKit.getActiveSessions(); + if (!(topic in wcPeers)) { + throw new WalletConnectError(`Unknown session ${topic}`, "UNAUTHORIZED_EVENT", null); + } + const session = wcPeers[topic]; + const accountPkh = session.namespaces.tezos.accounts[0].split(":")[2]; + const signer = getImplicitAccount(accountPkh); + const networkName = session.namespaces.tezos.chains?.[0].split(":")[1]; + const network = networkName ? findNetwork(networkName) : null; + if (!network) { + throw new WalletConnectError( + `Unsupported network ${networkName}`, + "UNSUPPORTED_CHAINS", + session + ); + } + const { publicKey, curve } = await getPublicKeyAndCurve( + accountPkh, + signer, + network ); + const response = formatJsonRpcResult(id, [ + { + algo: curve, + address: accountPkh, + pubkey: publicKey, + }, + ]); + await walletKit.respondSessionRequest({ topic, response }); + + toast({ + description: "Successfully provided the requested account data", + status: "success", + }); + return; } case "tezos_sign": { diff --git a/packages/core/src/getPublicKeyAndCurve.test.ts b/packages/core/src/getPublicKeyAndCurve.test.ts new file mode 100644 index 000000000..590cc9fe1 --- /dev/null +++ b/packages/core/src/getPublicKeyAndCurve.test.ts @@ -0,0 +1,99 @@ +import { getPublicKeyAndCurve } from "./getPublicKeyAndCurve"; +import { WalletConnectError } from "@umami/utils"; +import { makeToolkit } from "@umami/tezos"; +import { TezosToolkit } from "@taquito/taquito"; + +jest.mock("@umami/tezos", () => ({ + ...jest.requireActual("@umami/tezos"), + makeToolkit: jest.fn(), +})); +const mockGetManagerKey = jest.fn(); + +describe("getPublicKeyAndCurve", () => { + + beforeEach(() => { + const testToolkit = new TezosToolkit("test-tezos-toolkit"); + + jest.mocked(makeToolkit).mockImplementation( + () => + ({ + rpc: { + getManagerKey: mockGetManagerKey, + }, + }) as any + ); + jest.clearAllMocks(); + }); + + const mockSigner = { address: "tz1..." } as any; + const mockNetwork = { name: "mainnet" } as any; + const mockAddress = "tz1..."; + + it("returns the public key and curve for ed25519", async () => { + mockGetManagerKey.mockResolvedValue("edpk123456789"); + + const result = await getPublicKeyAndCurve(mockAddress, mockSigner, mockNetwork); + + expect(result).toEqual({ + publicKey: "edpk123456789", + curve: "ed25519", + }); + }); + + it("returns the public key and curve for secp256k1", async () => { + mockGetManagerKey.mockResolvedValue("sppk123456789"); + + const result = await getPublicKeyAndCurve(mockAddress, mockSigner, mockNetwork); + + expect(result).toEqual({ + publicKey: "sppk123456789", + curve: "secp256k1", + }); + }); + + it("returns the public key and curve for p-256", async () => { + mockGetManagerKey.mockResolvedValue("p2pk123456789"); + + const result = await getPublicKeyAndCurve(mockAddress, mockSigner, mockNetwork); + + expect(result).toEqual({ + publicKey: "p2pk123456789", + curve: "p-256", + }); + }); + + it("throws an error if the public key has an unknown prefix", async () => { + mockGetManagerKey.mockResolvedValue("unknown123456789"); + + await expect(getPublicKeyAndCurve(mockAddress, mockSigner, mockNetwork)).rejects.toThrow( + WalletConnectError + ); + + await expect(getPublicKeyAndCurve(mockAddress, mockSigner, mockNetwork)).rejects.toThrow( + "Unknown curve for the public key: unknown123456789" + ); + }); + + it("handles the case where the managerKeyResponse is an object with a key field", async () => { + mockGetManagerKey.mockResolvedValue({ key: "edpk987654321" }); + + const result = await getPublicKeyAndCurve(mockAddress, mockSigner, mockNetwork); + + expect(result).toEqual({ + publicKey: "edpk987654321", + curve: "ed25519", + }); + }); + + it("throws an error if the managerKeyResponse object does not have a key", async () => { + mockGetManagerKey.mockResolvedValue({}); + + await expect(getPublicKeyAndCurve(mockAddress, mockSigner, mockNetwork)).rejects.toThrow( + WalletConnectError + ); + + await expect(getPublicKeyAndCurve(mockAddress, mockSigner, mockNetwork)).rejects.toThrow( + `Signer address is not revealed on the ${mockNetwork.name}` + ); + }); +}); diff --git a/packages/core/src/getPublicKeyAndCurve.ts b/packages/core/src/getPublicKeyAndCurve.ts new file mode 100644 index 000000000..40dd104b1 --- /dev/null +++ b/packages/core/src/getPublicKeyAndCurve.ts @@ -0,0 +1,53 @@ +import { type ManagerKeyResponse } from "@taquito/rpc"; +import { type ImplicitAccount } from "@umami/core"; +import { type Network, type RawPkh, makeToolkit } from "@umami/tezos"; +import { WalletConnectError } from "@umami/utils"; + +/** + * Estimates (and simulates the execution of) the operations. + * + * @param address - tz1 address of the account + * @param signer - Implicit account + * @param network - network + * @returns the public key if revelead + * Throws an error if the account is not revelead + */ +export const getPublicKeyAndCurve = async ( + address: RawPkh, + signer: ImplicitAccount, + network: Network +): Promise<{ publicKey: string; curve: string }> => { + const tezosToolkit = await makeToolkit({ + type: "fake", + signer: signer, + network, + }); + const managerKeyResponse: ManagerKeyResponse = await tezosToolkit.rpc.getManagerKey(address); + let publicKey = ""; + if (typeof managerKeyResponse === "string") { + publicKey = managerKeyResponse; + } else if (managerKeyResponse.key) { + publicKey = managerKeyResponse.key; + } else { + throw new WalletConnectError( + `Signer address is not revealed on the ${network.name}`, + "UNSUPPORTED_ACCOUNTS", + null + ); + } + let curve = "unknown"; + if (publicKey.startsWith("edpk")) { + curve = "ed25519"; + } else if (publicKey.startsWith("sppk")) { + curve = "secp256k1"; + } else if (publicKey.startsWith("p2pk")) { + curve = "p-256"; + } else { + throw new WalletConnectError( + `Unknown curve for the public key: ${publicKey}`, + "UNSUPPORTED_ACCOUNTS", + null + ); + } + return { publicKey, curve }; +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 86c65c256..27c860924 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -5,6 +5,7 @@ export * from "./decodeBeaconPayload"; export * from "./Delegate"; export * from "./estimate"; export * from "./execute"; +export * from "./getPublicKeyAndCurve"; export * from "./helpers"; export * from "./Operation"; export * from "./testUtils";