From 755c1204b64f9640b0080832048ef559fccc3bd1 Mon Sep 17 00:00:00 2001 From: Marton Moro Date: Mon, 11 Nov 2024 16:47:49 +0100 Subject: [PATCH] chore(private-credential): add contract result handling --- apps/extension/src/sandbox/index.ts | 23 +- .../web-connector/routes/web-connector.tsx | 21 +- .../src/mina-network/mina-provider.ts | 206 +++++++++++------- packages/web-provider/src/utils/prompts.ts | 30 ++- 4 files changed, 182 insertions(+), 98 deletions(-) diff --git a/apps/extension/src/sandbox/index.ts b/apps/extension/src/sandbox/index.ts index 0c4bc4ec..2367d723 100644 --- a/apps/extension/src/sandbox/index.ts +++ b/apps/extension/src/sandbox/index.ts @@ -1,6 +1,8 @@ import { Add } from "@palladco/contracts" import { Credential } from "mina-credentials" +import { serializeError } from "serialize-error" import { match } from "ts-pattern" +import yaml from "yaml" import { z } from "zod" const EventTypeSchema = z.enum(["run"]) @@ -8,11 +10,15 @@ const ContractTypeSchema = z.enum(["add", "validate-credential"]) type ValidationResult = { type: "validate-credential-result" - success: boolean - credential?: string + result?: string error?: string } +const recoverOriginalPayload = (sanitizedPayload: string) => { + const parsedYaml = yaml.parse(sanitizedPayload) + return JSON.stringify(parsedYaml) +} + window.addEventListener("message", async (event) => { console.log(event.data) const type = EventTypeSchema.parse(event.data.type) @@ -34,24 +40,21 @@ window.addEventListener("message", async (event) => { }) .with("validate-credential", async () => { try { - const credentialInput = event.data.payload - const credentialDeserialized = Credential.fromJSON(credentialInput) + const sanitizedPayload = event.data.payload + const originalPayload = recoverOriginalPayload(sanitizedPayload) + const credentialDeserialized = Credential.fromJSON(originalPayload) await Credential.validate(credentialDeserialized) - // Use the existing ValidationResult type structure const result: ValidationResult = { type: "validate-credential-result", - success: true, - credential: Credential.toJSON(credentialDeserialized), + result: Credential.toJSON(credentialDeserialized), } window.parent.postMessage(result, "*") } catch (error: any) { const result: ValidationResult = { type: "validate-credential-result", - success: false, - error: - error instanceof Error ? error.message : "Validation Error", + error: serializeError(error), } window.parent.postMessage(result, "*") } diff --git a/packages/features/src/web-connector/routes/web-connector.tsx b/packages/features/src/web-connector/routes/web-connector.tsx index 0d6ff927..c8275e75 100644 --- a/packages/features/src/web-connector/routes/web-connector.tsx +++ b/packages/features/src/web-connector/routes/web-connector.tsx @@ -9,6 +9,12 @@ import yaml from "yaml" import type { UserInputForm } from "../types" import { WebConnectorView } from "../views/web-connector" +type ContractResult = { + type?: string + result?: string + error?: string +} + const sanitizePayload = async (payload: string) => { const parsedPayload = JSON.parse(payload) as Record const yamlPayload = yaml.stringify(parsedPayload) @@ -59,7 +65,7 @@ export const WebConnectorRoute = () => { } } const onSubmit: SubmitHandler< - UserInputForm & { contractResult?: Json } + UserInputForm & { contractResult?: ContractResult } > = async ({ userInput, contractResult }) => { const { id } = await windows.getCurrent() await runtime.sendMessage({ @@ -82,7 +88,9 @@ export const WebConnectorRoute = () => { setLoading(false) } } - const confirm = async ({ contractResult }: { contractResult?: Json }) => { + const confirm = async ({ + contractResult, + }: { contractResult?: ContractResult }) => { const { id } = await windows.getCurrent() await runtime.sendMessage({ userConfirmed: true, @@ -111,6 +119,15 @@ export const WebConnectorRoute = () => { window.close() } const eventListener = (event: MessageEvent) => { + if (event.data.type === "validate-credential-result") { + return confirm({ + contractResult: { + type: event.data.type, + result: event.data.result, + error: event.data.error, + }, + }) + } if (request.inputType === "confirmation") return confirm({ contractResult: event.data.result }) return onSubmit({ diff --git a/packages/web-provider/src/mina-network/mina-provider.ts b/packages/web-provider/src/mina-network/mina-provider.ts index 07d388ab..ccf141b5 100644 --- a/packages/web-provider/src/mina-network/mina-provider.ts +++ b/packages/web-provider/src/mina-network/mina-provider.ts @@ -37,20 +37,29 @@ const verifyInitialized = async () => { return !PalladApp.includes("UNINITIALIZED") } +const getPromptValue = async ( + promptPromise: Promise<{ value: T; contractResult?: any }>, +): Promise => { + const { value } = await promptPromise + return value +} + export const createMinaProvider = async (): Promise< MinaProviderClient & { emit: (type: any, event: any) => void } > => { const _emitter = mitt() const _vault = createVaultService() const unlockWallet = async () => { - const passphrase = await showUserPrompt({ - inputType: "password", - metadata: { - title: "Unlock your wallet", - submitButtonLabel: "Unlock", - rejectButtonLabel: "Cancel", - }, - }) + const passphrase = await getPromptValue( + showUserPrompt({ + inputType: "password", + metadata: { + title: "Unlock your wallet", + submitButtonLabel: "Unlock", + rejectButtonLabel: "Cancel", + }, + }), + ) if (passphrase === null) throw createProviderRpcError(4100, "Unauthorized") await _vault.unlockWallet(passphrase) } @@ -77,14 +86,16 @@ export const createMinaProvider = async (): Promise< } const enableOrigin = async ({ origin }: { origin: string }) => { await checkAndUnlockWallet() - const userConfirmed = await showUserPrompt({ - inputType: "confirmation", - metadata: { - title: "Connection request.", - payload: JSON.stringify({ origin }), - }, - emitConnected: true, - }) + const userConfirmed = await getPromptValue( + showUserPrompt({ + inputType: "confirmation", + metadata: { + title: "Connection request.", + payload: JSON.stringify({ origin }), + }, + emitConnected: true, + }), + ) if (!userConfirmed) { throw createProviderRpcError(4001, "User Rejected Request") } @@ -152,13 +163,15 @@ export const createMinaProvider = async (): Promise< if (!networkIds.includes(networkId)) { throw createProviderRpcError(4100, "Unauthorized.") } - const userConfirmed = await showUserPrompt({ - inputType: "confirmation", - metadata: { - title: "Switch to different chain.", - payload: JSON.stringify({ networkId }), - }, - }) + const userConfirmed = await getPromptValue( + showUserPrompt({ + inputType: "confirmation", + metadata: { + title: "Switch to different chain.", + payload: JSON.stringify({ networkId }), + }, + }), + ) if (!userConfirmed) { throw createProviderRpcError(4001, "User Rejected Request") } @@ -166,12 +179,14 @@ export const createMinaProvider = async (): Promise< return networkId }) .with({ method: "mina_requestNetwork" }, async () => { - const userConfirmed = await showUserPrompt({ - inputType: "confirmation", - metadata: { - title: "Request to current Mina network information.", - }, - }) + const userConfirmed = await getPromptValue( + showUserPrompt({ + inputType: "confirmation", + metadata: { + title: "Request to current Mina network information.", + }, + }), + ) if (!userConfirmed) { throw createProviderRpcError(4001, "User Rejected Request") } @@ -181,15 +196,16 @@ export const createMinaProvider = async (): Promise< .with( { method: P.union("mina_sign", "mina_signTransaction") }, async (signatureRequest) => { - const passphrase = await showUserPrompt({ - inputType: "password", - // TODO: Testing only - contract: "add", - metadata: { - title: "Signature request", - payload: JSON.stringify(signatureRequest.params), - }, - }) + const passphrase = await getPromptValue( + showUserPrompt({ + inputType: "password", + contract: "add", + metadata: { + title: "Signature request", + payload: JSON.stringify(signatureRequest.params), + }, + }), + ) if (passphrase === null) throw createProviderRpcError(4100, "Unauthorized.") const operationArgs: ChainOperationArgs = { @@ -229,13 +245,15 @@ export const createMinaProvider = async (): Promise< .with( { method: P.union("mina_createNullifier", "mina_signFields") }, async (signatureRequest) => { - const passphrase = await showUserPrompt({ - inputType: "password", - metadata: { - title: "Signature request", - payload: JSON.stringify(signatureRequest.params), - }, - }) + const passphrase = await getPromptValue( + showUserPrompt({ + inputType: "password", + metadata: { + title: "Signature request", + payload: JSON.stringify(signatureRequest.params), + }, + }), + ) if (passphrase === null) throw createProviderRpcError(4100, "Unauthorized.") const operationArgs: ChainOperationArgs = { @@ -305,13 +323,15 @@ export const createMinaProvider = async (): Promise< query as SearchQuery, props as string[], ) - const confirmation = await showUserPrompt({ - inputType: "confirmation", - metadata: { - title: "Credential read request", - payload: JSON.stringify({ ...params, credentials }), - }, - }) + const confirmation = await getPromptValue( + showUserPrompt({ + inputType: "confirmation", + metadata: { + title: "Credential read request", + payload: JSON.stringify({ ...params, credentials }), + }, + }), + ) if (!confirmation) { throw createProviderRpcError(4001, "User Rejected Request") } @@ -320,13 +340,15 @@ export const createMinaProvider = async (): Promise< .with({ method: "mina_setState" }, async ({ params }) => { const payload = params?.[0] if (!payload) throw createProviderRpcError(4000, "Invalid Request") - const confirmation = await showUserPrompt({ - inputType: "confirmation", - metadata: { - title: "Credential write request", - payload: JSON.stringify(payload), - }, - }) + const confirmation = await getPromptValue( + showUserPrompt({ + inputType: "confirmation", + metadata: { + title: "Credential write request", + payload: JSON.stringify(payload), + }, + }), + ) if (!confirmation) { throw createProviderRpcError(4001, "User Rejected Request") } @@ -339,38 +361,60 @@ export const createMinaProvider = async (): Promise< const stringifiedCredential = JSON.stringify(credential) - const confirmation = await showUserPrompt({ - inputType: "confirmation", - contract: "validate-credential", - metadata: { - title: "Store private credential request", - payload: stringifiedCredential, - }, - }) - - if (!confirmation) { - throw createProviderRpcError(4001, "User Rejected Request") - } try { - await _vault.storePrivateCredential(stringifiedCredential) - return { success: true } + const { value: userConfirmed, contractResult } = + await showUserPrompt({ + inputType: "confirmation", + contract: "validate-credential", + metadata: { + title: "Store private credential request", + payload: stringifiedCredential, + }, + }) + + if (!userConfirmed) { + throw createProviderRpcError(4001, "User Rejected Request") + } + + if (contractResult?.error) { + throw createProviderRpcError( + 4100, + `Credential validation failed: ${contractResult.error}`, + ) + } + + if (!contractResult?.result) { + throw createProviderRpcError(4100, "Missing validation result") + } + + try { + await _vault.storePrivateCredential(contractResult.result) + return { success: contractResult.result } + } catch (error: any) { + throw createProviderRpcError( + 4100, + `Failed to store private credential: ${error}`, + ) + } } catch (error: any) { throw createProviderRpcError( 4100, - `Failed to store private credential: ${error}`, + error.message || "Failed to validate credential", ) } }) .with({ method: "mina_sendTransaction" }, async ({ params }) => { const [payload] = params if (!payload) throw createProviderRpcError(4000, "Invalid Request") - const passphrase = await showUserPrompt({ - inputType: "password", - metadata: { - title: "Send transaction request", - payload: JSON.stringify(payload), - }, - }) + const passphrase = await getPromptValue( + showUserPrompt({ + inputType: "password", + metadata: { + title: "Send transaction request", + payload: JSON.stringify(payload), + }, + }), + ) if (passphrase === null) throw createProviderRpcError(4100, "Unauthorized.") try { diff --git a/packages/web-provider/src/utils/prompts.ts b/packages/web-provider/src/utils/prompts.ts index 428b5d98..e03e06cf 100644 --- a/packages/web-provider/src/utils/prompts.ts +++ b/packages/web-provider/src/utils/prompts.ts @@ -6,6 +6,17 @@ type Metadata = { payload?: string } +type ContractResult = { + type?: string + result?: string + error?: string +} + +type PromptResult = { + value: T + contractResult?: ContractResult +} + export const showUserPrompt = async ({ inputType, metadata, @@ -16,8 +27,8 @@ export const showUserPrompt = async ({ metadata: Metadata contract?: string emitConnected?: boolean -}): Promise => { - return new Promise((resolve, reject) => { +}): Promise> => { + return new Promise>((resolve, reject) => { chrome.windows .create({ url: "prompt.html", @@ -34,11 +45,20 @@ export const showUserPrompt = async ({ return reject(new Error("4001 - User Rejected Request")) } if (inputType === "confirmation") { - if (response.userConfirmed) return resolve(true as any) + if (response.userConfirmed) { + return resolve({ + value: true as T, + contractResult: response.contractResult, + }) + } return reject(new Error("4001 - User Rejected Request")) } - if (response.userInput.length > 0) - return resolve(response.userInput) + if (response.userInput.length > 0) { + return resolve({ + value: response.userInput, + contractResult: response.contractResult, + }) + } return reject(new Error("4100 - Unauthorized")) } return reject(new Error("Wrong window context"))