From 041832659a526fcee76f194ee89d8b5ac43340ea Mon Sep 17 00:00:00 2001 From: Peter Ferguson Date: Sun, 17 Mar 2024 01:25:22 +0000 Subject: [PATCH 1/5] move spc utilites into lib --- apps/dapp/package.json | 1 + .../src/components/create-payment-button.tsx | 107 +------- apps/dapp/src/components/nft-image-card.tsx | 34 ++- .../src/components/wallet-iframe-dialog.astro | 4 +- apps/dapp/src/lib/example-nft.ts | 12 +- apps/dapp/src/lib/safe-account.ts | 56 ++--- apps/dapp/src/lib/utils.ts | 134 +--------- apps/dapp/src/pages/index.astro | 37 ++- packages/helpers/index.ts | 2 +- packages/helpers/tsconfig.json | 23 ++ .../src/lib => packages/spc-lib}/constants.ts | 0 packages/spc-lib/index.ts | 228 ++++++++++++++++++ packages/spc-lib/package.json | 15 ++ packages/spc-lib/tsconfig.json | 25 ++ packages/{helpers => spc-lib}/validators.ts | 0 pnpm-lock.yaml | 12 + 16 files changed, 392 insertions(+), 298 deletions(-) create mode 100644 packages/helpers/tsconfig.json rename {apps/dapp/src/lib => packages/spc-lib}/constants.ts (100%) create mode 100644 packages/spc-lib/index.ts create mode 100644 packages/spc-lib/package.json create mode 100644 packages/spc-lib/tsconfig.json rename packages/{helpers => spc-lib}/validators.ts (100%) diff --git a/apps/dapp/package.json b/apps/dapp/package.json index 7167869..f760b8e 100644 --- a/apps/dapp/package.json +++ b/apps/dapp/package.json @@ -26,6 +26,7 @@ "permissionless": "^0.1.10", "react": "^18.2.0", "react-dom": "^18.2.0", + "spc-lib": "workspace:*", "tailwind-merge": "^2.2.1", "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", diff --git a/apps/dapp/src/components/create-payment-button.tsx b/apps/dapp/src/components/create-payment-button.tsx index d03520d..8035ccd 100644 --- a/apps/dapp/src/components/create-payment-button.tsx +++ b/apps/dapp/src/components/create-payment-button.tsx @@ -1,98 +1,11 @@ import { Button } from "@/components/ui/button"; -import { WALLET_IFRAME_DIALOG_ID } from "@/lib/constants"; -import { getPaymentDetails, getPaymentOrigin, payWithSPC } from "@/lib/utils"; -import { getAllowedCredentialsSchema } from "helpers"; - -const getIframe = () => { - const iframeDialog = document.getElementById( - WALLET_IFRAME_DIALOG_ID, - ) as HTMLIFrameElement | null; - - const iframe = iframeDialog?.querySelector("iframe"); - - return iframe; -}; - -const fallbackToIframeCredentialCreation = async () => { - // - trigger the iframe container to open so the user can create a credential - const iframeDialog = document.getElementById( - WALLET_IFRAME_DIALOG_ID, - ) as HTMLDialogElement | null; - - iframeDialog?.classList.remove("hidden"); - iframeDialog?.classList.add("flex"); - - iframeDialog?.showModal(); -}; - -/** - * A utility function to wait for a message from the wallet iframe - */ -const createMessagePromise = (eventType: string): Promise => { - const iframe = getIframe(); - - if (!iframe) throw new Error("No iframe found"); - - const iframeOrigin = iframe?.src ? new URL(iframe.src).origin : undefined; - - let timeout: NodeJS.Timeout | undefined; - const credentialsPromise: Promise = new Promise( - (resolve, reject) => { - // biome-ignore lint/suspicious/noExplicitAny: - function handleMessage(event: any) { - const eventOrigin = new URL(event.origin).origin; - if (eventOrigin !== iframeOrigin) return; - - console.log("received message", event); - - if (event.data.type === eventType) { - resolve(event.data); - } - // Remove the event listener to clean up - window.removeEventListener("message", handleMessage); - } - - // Add an event listener to listen for messages from the iframe - window.addEventListener("message", handleMessage, false); - - // Set a timeout to reject the promise if no response is received within a specific timeframe - timeout = setTimeout(() => { - window.removeEventListener("message", handleMessage); - reject(new Error("Timeout waiting for credentials response")); - }, 10000); // 10 seconds timeout - }, - ); - - // ! Ensure to clean up on promise resolution or rejection - credentialsPromise.finally(() => clearTimeout(timeout)); - - return credentialsPromise; -}; - -/** - * Get the available credentials from the wallet iframe - */ -const getAvailableCredentials = async () => { - const iframe = getIframe(); - - if (!iframe) throw new Error("No iframe found"); - - // - Create a Promise that resolves when the expected message is received from the wallet iframe - const waitForCredentials = async () => { - const eventData = await createMessagePromise("credentials.get"); - const parsedMessage = getAllowedCredentialsSchema.parse(eventData); - - return parsedMessage.credentials; - }; - - // - Ask the iframe for the available credentials - iframe.contentWindow?.postMessage( - { type: "credentials.get" }, - getPaymentOrigin(), - ); - - return await waitForCredentials(); -}; +import { getPayeeOrigin, getPaymentOrigin } from "@/lib/utils"; +import { + getPaymentDetails, + payWithSPC, + getAvailableCredentials, + fallbackToIframeCredentialCreation, +} from "spc-lib"; export function CreatePaymentButton() { return ( @@ -101,7 +14,9 @@ export function CreatePaymentButton() { onClick={async (e) => { e.preventDefault(); - const allowedCredentials = await getAvailableCredentials(); + const allowedCredentials = await getAvailableCredentials( + getPaymentOrigin(), + ); console.log("allowedCredentials", allowedCredentials); @@ -111,10 +26,12 @@ export function CreatePaymentButton() { await payWithSPC( { + rpId: getPaymentOrigin().replace("https://", ""), allowedCredentials, challenge: "challenge", timeout: 60000, }, + getPayeeOrigin(), getPaymentDetails("0.0000001"), "0x123...456", ); diff --git a/apps/dapp/src/components/nft-image-card.tsx b/apps/dapp/src/components/nft-image-card.tsx index 9e20007..6a86d12 100644 --- a/apps/dapp/src/components/nft-image-card.tsx +++ b/apps/dapp/src/components/nft-image-card.tsx @@ -1,20 +1,27 @@ -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { getNftUri, getBalance } from "@/lib/example-nft"; import React from "react"; +import { CreatePaymentButton } from "./create-payment-button"; export function NftImageCard() { - const tokenId = 1n; const [uri, setUri] = React.useState(null); const [hasClaimed, setHasClaimed] = React.useState(false); React.useEffect(() => { - getNftUri(0).then((r) => setUri(r)); + getNftUri(0n).then((r) => setUri(r)); }, []); React.useEffect(() => { - getBalance('0x3896a2938d345d3A351cE152AF1b8Cb17bb006be', 0n).then((r) => { + getBalance("0x3896a2938d345d3A351cE152AF1b8Cb17bb006be", 0n).then((r) => { console.log({ r }); - setHasClaimed(r > 0) + setHasClaimed(r > 0); }); }, []); @@ -22,13 +29,20 @@ export function NftImageCard() { 10 Eth Coupon - Claim your free 10 eth coupon which can + + Claim your free 10 eth coupon which can + - - NFT + + {uri && NFT} - -

You have {hasClaimed ? '' : 'not'} claimed

+ + {hasClaimed ? ( +

You have claimed

+ // TODO: add the txHash of the claim + ) : ( + + )}
); diff --git a/apps/dapp/src/components/wallet-iframe-dialog.astro b/apps/dapp/src/components/wallet-iframe-dialog.astro index e4d5b91..c02703c 100644 --- a/apps/dapp/src/components/wallet-iframe-dialog.astro +++ b/apps/dapp/src/components/wallet-iframe-dialog.astro @@ -1,6 +1,6 @@ --- import { cn } from "@/lib/utils"; -import { WALLET_IFRAME_DIALOG_ID } from "@/lib/constants"; +import { WALLET_IFRAME_DIALOG_ID } from "spc-lib"; import { getPaymentOrigin } from "@/lib/utils"; const paymentOrigin = getPaymentOrigin(); @@ -21,7 +21,7 @@ const paymentOrigin = getPaymentOrigin(); allow={`payment ${paymentOrigin}`}> diff --git a/apps/wallet/src/pages/index.astro b/apps/wallet/src/pages/index.astro index 4f56edd..d27a68c 100644 --- a/apps/wallet/src/pages/index.astro +++ b/apps/wallet/src/pages/index.astro @@ -27,19 +27,25 @@ import { UsernameForm } from "../components/forms/username"; diff --git a/packages/helpers/index.ts b/packages/helpers/index.ts index fdfa61c..437e282 100644 --- a/packages/helpers/index.ts +++ b/packages/helpers/index.ts @@ -1,5 +1,4 @@ export * from "./base64url"; export * from "./domains"; export * from "./is-spc-available"; -export * from "../spc-lib/validators"; export * from "./webauthn"; diff --git a/packages/spc-lib/index.ts b/packages/spc-lib/index.ts deleted file mode 100644 index a582bf0..0000000 --- a/packages/spc-lib/index.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { WALLET_IFRAME_DIALOG_ID } from "./constants"; -import { getAllowedCredentialsSchema } from "./validators"; -import { defaultInstrument, fromBuffer, toBuffer } from "helpers"; - -export * from "./constants"; - -/** - * A utility function to trigger the wallet iframe to open for credential creation - */ -export const fallbackToIframeCredentialCreation = async () => { - // - trigger the iframe container to open so the user can create a credential - const iframeDialog = document.getElementById( - WALLET_IFRAME_DIALOG_ID, - ) as HTMLDialogElement | null; - - iframeDialog?.classList.remove("hidden"); - iframeDialog?.classList.add("flex"); - - iframeDialog?.showModal(); -}; - -/** - * A utility function to get the iframe element - * - * @returns The wallet iframe - */ -export const getIframe = () => { - const iframeDialog = document.getElementById( - WALLET_IFRAME_DIALOG_ID, - ) as HTMLIFrameElement | null; - - const iframe = iframeDialog?.querySelector("iframe"); - - return iframe; -}; - -/** - * A utility function to wait for a message from the wallet iframe - */ -export const createMessagePromise = (eventType: string): Promise => { - const iframe = getIframe(); - - if (!iframe) throw new Error("No iframe found"); - - const iframeOrigin = iframe?.src ? new URL(iframe.src).origin : undefined; - - let timeout: number | undefined; - const credentialsPromise: Promise = new Promise( - (resolve, reject) => { - // biome-ignore lint/suspicious/noExplicitAny: - function handleMessage(event: any) { - const eventOrigin = new URL(event.origin).origin; - if (eventOrigin !== iframeOrigin) return; - - console.log("received message", event); - - if (event.data.type === eventType) { - resolve(event.data); - } - // Remove the event listener to clean up - window.removeEventListener("message", handleMessage); - } - - // Add an event listener to listen for messages from the iframe - window.addEventListener("message", handleMessage, false); - - // Set a timeout to reject the promise if no response is received within a specific timeframe - timeout = setTimeout(() => { - window.removeEventListener("message", handleMessage); - reject(new Error("Timeout waiting for credentials response")); - }, 10000); // 10 seconds timeout - }, - ); - - // ! Ensure to clean up on promise resolution or rejection - credentialsPromise.finally(() => clearTimeout(timeout)); - - return credentialsPromise; -}; - -/** - * Get the available credentials from the wallet iframe - * - * @param origin The origin of the wallet iframe - * @returns The available credentials - */ -export const getAvailableCredentials = async (origin: string) => { - const iframe = getIframe(); - - if (!iframe) throw new Error("No iframe found"); - - // - Create a Promise that resolves when the expected message is received from the wallet iframe - const waitForCredentials = async () => { - const eventData = await createMessagePromise("credentials.get"); - const parsedMessage = getAllowedCredentialsSchema.parse(eventData); - - return parsedMessage.credentials; - }; - - // - Ask the iframe for the available credentials - iframe.contentWindow?.postMessage({ type: "credentials.get" }, origin); - - return await waitForCredentials(); -}; - -type Address = `0x${string}`; -type SerialisableCredentialRequestOptions = Omit< - Partial, - "allowedCredentials" | "challenge" -> & { - challenge: string; - /** - * The allowed credential ids that are allowed to be used for the payment. Simplified to be only the base64url id - */ - allowedCredentials: string[]; -}; - -export const getPaymentDetails = (amount: string) => ({ - // - the SPC spec allows for multiple items to be displayed - // - but currently the Chrome implementation does NOT display ANY items - displayItems: [ - { - label: "NFT", - amount: { currency: "USD", value: "0.0000001" }, - pending: true, - }, - { - label: "Gas Fee", - amount: { currency: "USD", value: "0.0000001" }, - pending: true, - }, - ], - total: { - label: "Total", - // - currency must be a ISO 4217 currency code - amount: { currency: "USD", value: amount }, - }, -}); - -export const payWithSPC = async ( - requestOptions: SerialisableCredentialRequestOptions, - payeeOrigin: string, - paymentDetails: PaymentDetailsInit, - address: Address, -) => { - const { challenge, timeout } = requestOptions; - - // - convert the allowed credential ids to a buffer - const credentialIds = requestOptions.allowedCredentials - .map((credentialId) => { - const credential = toBuffer(credentialId); - - // - verify credentialId is reasonable - if (credential.byteLength > 32) return undefined; - - return credential; - }) - .filter(Boolean); - - const paymentMethodData = [ - { - // Specify `secure-payment-confirmation` as payment method. - supportedMethods: "secure-payment-confirmation", - data: { - // The RP ID - rpId: requestOptions.rpId, - - // List of credential IDs obtained from the RP. - credentialIds, - - // The challenge is also obtained from the RP. - challenge: toBuffer(challenge), - - // A display name and an icon that represent the payment instrument. - instrument: { - ...defaultInstrument, - displayName: `Sent From Wallet - ${address}`, - }, - - // The origin of the payee - payeeOrigin, - - // The number of milliseconds to timeout. - timeout, - }, - }, - ]; - - console.log("spc paymentMethodData", paymentMethodData, paymentDetails); - - const request = new PaymentRequest(paymentMethodData, paymentDetails); - - console.log("spc request", JSON.stringify(request)); - - // biome-ignore lint/suspicious/noExplicitAny: - let response: any; - try { - response = await request.show(); - - console.log("spc response", response); - - // response.details is a PublicKeyCredential, with a clientDataJSON that - // contains the transaction data for verification by the issuing bank. - const cred = response.details; - - // TODO: send the wallet the response for verification before executing the payment - const serialisableCredential = { - id: cred.id, - type: cred.type, - // credential.rawId = base64url.encode(cred.rawId); - response: { - clientDataJSON: fromBuffer(cred.response.clientDataJSON), - authenticatorData: fromBuffer(cred.response.authenticatorData), - signature: fromBuffer(cred.response.signature), - userHandle: fromBuffer(cred.response.userHandle), - }, - }; - - await response.complete("success"); - - /* send response.details to the issuing bank for verification */ - } catch (err) { - await response.complete("fail"); - /* SPC cannot be used; merchant should fallback to traditional flows */ - console.error((err as Error).message); - throw err; - } -}; diff --git a/packages/spc-lib/package.json b/packages/spc-lib/package.json index 000ca30..89c2bc9 100644 --- a/packages/spc-lib/package.json +++ b/packages/spc-lib/package.json @@ -2,7 +2,7 @@ "name": "spc-lib", "version": "1.0.0", "description": "", - "main": "index.ts", + "main": "src/index.ts", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/packages/spc-lib/constants.ts b/packages/spc-lib/src/constants.ts similarity index 100% rename from packages/spc-lib/constants.ts rename to packages/spc-lib/src/constants.ts diff --git a/packages/spc-lib/src/index.ts b/packages/spc-lib/src/index.ts new file mode 100644 index 0000000..3c9d681 --- /dev/null +++ b/packages/spc-lib/src/index.ts @@ -0,0 +1,4 @@ +export * from "./constants"; +export * from "./utils"; +export * from "./validators"; +export * from "./pay-with-spc"; diff --git a/packages/spc-lib/src/pay-with-spc.ts b/packages/spc-lib/src/pay-with-spc.ts new file mode 100644 index 0000000..08d1487 --- /dev/null +++ b/packages/spc-lib/src/pay-with-spc.ts @@ -0,0 +1,103 @@ +import { defaultInstrument, fromBuffer, toBuffer } from "helpers"; + +export type Address = `0x${string}`; +export type SerialisableCredentialRequestOptions = Omit< + Partial, + "allowedCredentials" | "challenge" +> & { + challenge: string; + /** + * The allowed credential ids that are allowed to be used for the payment. Simplified to be only the base64url id + */ + allowedCredentials: string[]; +}; + +export const payWithSPC = async ( + requestOptions: SerialisableCredentialRequestOptions, + payeeOrigin: string, + paymentDetails: PaymentDetailsInit, + address: Address, +) => { + const { challenge, timeout } = requestOptions; + + // - convert the allowed credential ids to a buffer + const credentialIds = requestOptions.allowedCredentials + .map((credentialId) => { + const credential = toBuffer(credentialId); + + // - verify credentialId is reasonable + if (credential.byteLength > 32) return undefined; + + return credential; + }) + .filter(Boolean); + + const paymentMethodData = [ + { + // Specify `secure-payment-confirmation` as payment method. + supportedMethods: "secure-payment-confirmation", + data: { + // The RP ID + rpId: requestOptions.rpId, + + // List of credential IDs obtained from the RP. + credentialIds, + + // The challenge is also obtained from the RP. + challenge: toBuffer(challenge), + + // A display name and an icon that represent the payment instrument. + instrument: { + ...defaultInstrument, + displayName: `Sent From Wallet - ${address}`, + }, + + // The origin of the payee + payeeOrigin, + + // The number of milliseconds to timeout. + timeout, + }, + }, + ]; + + console.log("spc paymentMethodData", paymentMethodData, paymentDetails); + + const request = new PaymentRequest(paymentMethodData, paymentDetails); + + console.log("spc request", JSON.stringify(request)); + + // biome-ignore lint/suspicious/noExplicitAny: + let response: any; + try { + response = await request.show(); + + console.log("spc response", response); + + // response.details is a PublicKeyCredential, with a clientDataJSON that + // contains the transaction data for verification by the issuing bank. + const cred = response.details; + + // TODO: send the wallet the response for verification before executing the payment + const serialisableCredential = { + id: cred.id, + type: cred.type, + // credential.rawId = base64url.encode(cred.rawId); + response: { + clientDataJSON: fromBuffer(cred.response.clientDataJSON), + authenticatorData: fromBuffer(cred.response.authenticatorData), + signature: fromBuffer(cred.response.signature), + userHandle: fromBuffer(cred.response.userHandle), + }, + }; + + await response.complete("success"); + + /* send response.details to the issuing bank for verification */ + } catch (err) { + await response.complete("fail"); + /* SPC cannot be used; merchant should fallback to traditional flows */ + console.error((err as Error).message); + throw err; + } +}; diff --git a/packages/spc-lib/src/utils/create-message-promise.ts b/packages/spc-lib/src/utils/create-message-promise.ts new file mode 100644 index 0000000..4ece06c --- /dev/null +++ b/packages/spc-lib/src/utils/create-message-promise.ts @@ -0,0 +1,46 @@ +import { getIframe } from "./get-Iframe"; + +/** + * A utility function to wait for a message from the wallet iframe + */ + +export const createMessagePromise = (eventType: string): Promise => { + const iframe = getIframe(); + + if (!iframe) throw new Error("No iframe found"); + + const iframeOrigin = iframe?.src ? new URL(iframe.src).origin : undefined; + + let timeout: number | undefined; + const credentialsPromise: Promise = new Promise( + (resolve, reject) => { + // biome-ignore lint/suspicious/noExplicitAny: + function handleMessage(event: any) { + const eventOrigin = new URL(event.origin).origin; + if (eventOrigin !== iframeOrigin) return; + + console.log("received message", event); + + if (event.data.type === eventType) { + resolve(event.data); + } + // Remove the event listener to clean up + window.removeEventListener("message", handleMessage); + } + + // Add an event listener to listen for messages from the iframe + window.addEventListener("message", handleMessage, false); + + // Set a timeout to reject the promise if no response is received within a specific timeframe + timeout = setTimeout(() => { + window.removeEventListener("message", handleMessage); + reject(new Error("Timeout waiting for credentials response")); + }, 10000); // 10 seconds timeout + }, + ); + + // ! Ensure to clean up on promise resolution or rejection + credentialsPromise.finally(() => clearTimeout(timeout)); + + return credentialsPromise; +}; diff --git a/packages/spc-lib/src/utils/fallback-to-iframe-credential-creation.ts b/packages/spc-lib/src/utils/fallback-to-iframe-credential-creation.ts new file mode 100644 index 0000000..f014c67 --- /dev/null +++ b/packages/spc-lib/src/utils/fallback-to-iframe-credential-creation.ts @@ -0,0 +1,17 @@ +import { WALLET_IFRAME_DIALOG_ID } from "../constants"; + +/** + * A utility function to trigger the wallet iframe to open for credential creation + */ + +export const fallbackToIframeCredentialCreation = async () => { + // - trigger the iframe container to open so the user can create a credential + const iframeDialog = document.getElementById( + WALLET_IFRAME_DIALOG_ID, + ) as HTMLDialogElement | null; + + iframeDialog?.classList.remove("hidden"); + iframeDialog?.classList.add("flex"); + + iframeDialog?.showModal(); +}; diff --git a/packages/spc-lib/src/utils/get-Iframe.ts b/packages/spc-lib/src/utils/get-Iframe.ts new file mode 100644 index 0000000..b89ef9f --- /dev/null +++ b/packages/spc-lib/src/utils/get-Iframe.ts @@ -0,0 +1,17 @@ +import { WALLET_IFRAME_DIALOG_ID } from "../constants"; + +/** + * A utility function to get the iframe element + * + * @returns The wallet iframe + */ + +export const getIframe = () => { + const iframeDialog = document.getElementById( + WALLET_IFRAME_DIALOG_ID, + ) as HTMLIFrameElement | null; + + const iframe = iframeDialog?.querySelector("iframe"); + + return iframe; +}; diff --git a/packages/spc-lib/src/utils/get-available-credentials.ts b/packages/spc-lib/src/utils/get-available-credentials.ts new file mode 100644 index 0000000..c52887e --- /dev/null +++ b/packages/spc-lib/src/utils/get-available-credentials.ts @@ -0,0 +1,29 @@ +import { getAllowedCredentialsSchema } from "../validators"; +import { getIframe } from "./get-Iframe"; +import { createMessagePromise } from "./create-message-promise"; + +/** + * Get the available credentials from the wallet iframe + * + * @param origin The origin of the wallet iframe + * @returns The available credentials + */ + +export const getAvailableCredentials = async (origin: string) => { + const iframe = getIframe(); + + if (!iframe) throw new Error("No iframe found"); + + // - Create a Promise that resolves when the expected message is received from the wallet iframe + const waitForCredentials = async () => { + const eventData = await createMessagePromise("credentials.get"); + const parsedMessage = getAllowedCredentialsSchema.parse(eventData); + + return parsedMessage.credentials; + }; + + // - Ask the iframe for the available credentials + iframe.contentWindow?.postMessage({ type: "credentials.get" }, origin); + + return await waitForCredentials(); +}; diff --git a/packages/spc-lib/src/utils/get-payment-details.ts b/packages/spc-lib/src/utils/get-payment-details.ts new file mode 100644 index 0000000..7a57776 --- /dev/null +++ b/packages/spc-lib/src/utils/get-payment-details.ts @@ -0,0 +1,21 @@ +export const getPaymentDetails = (amount: string) => ({ + // - the SPC spec allows for multiple items to be displayed + // - but currently the Chrome implementation does NOT display ANY items + displayItems: [ + { + label: "NFT", + amount: { currency: "USD", value: "0.0000001" }, + pending: true, + }, + { + label: "Gas Fee", + amount: { currency: "USD", value: "0.0000001" }, + pending: true, + }, + ], + total: { + label: "Total", + // - currency must be a ISO 4217 currency code + amount: { currency: "USD", value: amount }, + }, +}); diff --git a/packages/spc-lib/src/utils/index.ts b/packages/spc-lib/src/utils/index.ts new file mode 100644 index 0000000..5eab28c --- /dev/null +++ b/packages/spc-lib/src/utils/index.ts @@ -0,0 +1,5 @@ +export * from "./create-message-promise"; +export * from "./fallback-to-iframe-credential-creation"; +export * from "./get-available-credentials"; +export * from "./get-Iframe"; +export * from "./get-payment-details"; diff --git a/packages/spc-lib/validators.ts b/packages/spc-lib/src/validators.ts similarity index 68% rename from packages/spc-lib/validators.ts rename to packages/spc-lib/src/validators.ts index 605182f..0f9907d 100644 --- a/packages/spc-lib/validators.ts +++ b/packages/spc-lib/src/validators.ts @@ -6,5 +6,7 @@ export const getAllowedCredentialsMessageTypeSchema = z.enum([ export const getAllowedCredentialsSchema = z.object({ type: getAllowedCredentialsMessageTypeSchema, - credentials: z.array(z.string()), + credentials: z.array( + z.object({ credentialId: z.string(), address: z.string().optional() }), + ), }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3d0e58..73b62fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ importers: typescript: specifier: ^5.4.2 version: 5.4.2 + usehooks-ts: + specifier: ^3.0.1 + version: 3.0.1(react@18.2.0) viem: specifier: ^2.8.10 version: 2.8.10(typescript@5.4.2)(zod@3.22.4) @@ -5307,6 +5310,10 @@ packages: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} dev: false + /lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + dev: false + /lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} dev: false @@ -8354,6 +8361,16 @@ packages: resolution: {integrity: sha512-H6dnQ/yPAAVzMQRvEvyz01hhfQL5qRWSEt7BX8t9DqnPw9BjMb64fjIRq76Uvf1hkHp+mTZvEVJ5guXOT0Xqaw==} dev: false + /usehooks-ts@3.0.1(react@18.2.0): + resolution: {integrity: sha512-bgJ8S9w/SnQyACd3RvWp3CGncROxEENGqQLCsdaoyTb0zTENIna7MIV3OW6ywCfPaYYD2OPokw7oLPmSLLWP4w==} + engines: {node: '>=16.15.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + dependencies: + lodash.debounce: 4.0.8 + react: 18.2.0 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: false From 5b6b3382b1b92e6ee0c26dc417b315dac1b5ee9a Mon Sep 17 00:00:00 2001 From: Peter Ferguson Date: Sun, 17 Mar 2024 05:48:41 +0000 Subject: [PATCH 3/5] wip get sigs working on v7 passkey safe --- .../src/components/create-payment-button.tsx | 118 ++++++++++++------ apps/dapp/src/lib/permissionless.ts | 7 +- apps/wallet/src/pages/relay/deploy.ts | 48 ++++++- packages/spc-lib/src/pay-with-spc.ts | 5 +- 4 files changed, 132 insertions(+), 46 deletions(-) diff --git a/apps/dapp/src/components/create-payment-button.tsx b/apps/dapp/src/components/create-payment-button.tsx index 1146b4d..e554cdc 100644 --- a/apps/dapp/src/components/create-payment-button.tsx +++ b/apps/dapp/src/components/create-payment-button.tsx @@ -1,18 +1,22 @@ import { Button } from "@/components/ui/button"; import { formatMintCouponToModuleExecutionCalldata } from "@/lib/example-nft"; import { - getUserOperationHash, + bundlerClient, + paymasterClient, + publicClient, +} from "@/lib/permissionless"; +import { getPayeeOrigin, getPaymentOrigin } from "@/lib/utils"; +import { ENTRYPOINT_ADDRESS_V07, getAccountNonce, + getUserOperationHash, type UserOperation, } from "permissionless"; -import { bundlerClient, publicClient } from "@/lib/permissionless"; -import { getPayeeOrigin, getPaymentOrigin } from "@/lib/utils"; import { + fallbackToIframeCredentialCreation, + getAvailableCredentials, getPaymentDetails, payWithSPC, - getAvailableCredentials, - fallbackToIframeCredentialCreation, } from "spc-lib"; import type { Address } from "viem"; import { baseSepolia } from "viem/chains"; @@ -33,39 +37,12 @@ export function CreatePaymentButton() { return fallbackToIframeCredentialCreation(); } - const nonce = await getAccountNonce(publicClient, { - sender: address, - entryPoint: ENTRYPOINT_ADDRESS_V07, - }); - - console.log("nonce ", nonce); - - const userOperation = { - sender: address, - nonce, - callData: formatMintCouponToModuleExecutionCalldata(), - // accountGasLimits: 8000000n, - callGasLimit: 500305n, - verificationGasLimit: 8005650n, - preVerificationGas: 506135n, - maxFeePerGas: 113000000n, - maxPriorityFeePerGas: 113000100n, - signature: "0x" as const, - } satisfies UserOperation<"v0.7">; - - const gasEstimate = await bundlerClient.estimateUserOperationGas({ - userOperation, - }); - - console.log("gasEstimate ", gasEstimate); - - const userOperationHash = getUserOperationHash({ - userOperation, - chainId: baseSepolia.id, - entryPoint: ENTRYPOINT_ADDRESS_V07, - }); + const { userOperation, userOperationHash } = + await getMintNftUserOperation(address); - await payWithSPC( + const { + response: { signature }, + } = await payWithSPC( { rpId: getPaymentOrigin().replace("https://", ""), allowedCredentials: [credential.credentialId], @@ -76,9 +53,76 @@ export function CreatePaymentButton() { getPaymentDetails("0.0000001"), address, ); + + await bundlerClient.sendUserOperation({ + userOperation: { + ...userOperation, + signature: `0x${signature}`, + }, + }); }} > Create Payment ); } + +const getMintNftUserOperation = async (address: Address) => { + const nonce = await getAccountNonce(publicClient, { + sender: address, + entryPoint: ENTRYPOINT_ADDRESS_V07, + }); + + console.log("nonce ", nonce); + + let userOperation: UserOperation<"v0.7"> = { + sender: address, + nonce, + callData: formatMintCouponToModuleExecutionCalldata(), + // accountGasLimits: 8000000n, + callGasLimit: 900080n, + verificationGasLimit: 8005650n, + preVerificationGas: 801330n, + + maxFeePerGas: 113000000n, + maxPriorityFeePerGas: 113000100n, + signature: "0x", + }; + + // const gasEstimate = await bundlerClient.estimateUserOperationGas({ + // userOperation, + // }); + + // console.log("gasEstimate ", gasEstimate); + + // const paymasterData = await paymasterClient.sponsorUserOperation({ + // userOperation, + // }); + + // console.log("paymasterData", paymasterData); + + // userOperation = { ...userOperation, ...paymasterData }; + + // const userOperation = { + // callData: + // "0x7bb374280000000000000000000000004a56fd1d63d99978fdb3ac5c152ea122514b6792000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000443efc46400000000000000000000000000000000000000000000000000000000", + // initCode: "0x", + // callGasLimit: "117688", + // verificationGasLimit: "61421", + // preVerificationGas: "285339", + // maxFeePerGas: "75550508", + // maxPriorityFeePerGas: "110000", + // nonce: "0", + // paymasterAndData: "0x", + // sender: "0x2fe7892721d8279cec0f8687c4b2e922ca7b76b0", + // signature: "0x", + // }; + + const userOperationHash = getUserOperationHash({ + userOperation, + chainId: baseSepolia.id, + entryPoint: ENTRYPOINT_ADDRESS_V07, + }); + + return { userOperation, userOperationHash }; +}; diff --git a/apps/dapp/src/lib/permissionless.ts b/apps/dapp/src/lib/permissionless.ts index 6db1714..bcd76e2 100644 --- a/apps/dapp/src/lib/permissionless.ts +++ b/apps/dapp/src/lib/permissionless.ts @@ -1,4 +1,4 @@ -import { createBundlerClient } from "permissionless"; +import { ENTRYPOINT_ADDRESS_V07, createBundlerClient } from "permissionless"; import { pimlicoPaymasterActions } from "permissionless/actions/pimlico"; import { createClient, createPublicClient, http } from "viem"; import { baseSepolia } from "viem/chains"; @@ -11,16 +11,13 @@ const pimlicoTransport = (version: "v1" | "v2") => `https://api.pimlico.io/${version}/${baseSepolia.id}/rpc?apikey=${pimlicoApiKey}`, ); -export const ENTRYPOINT_ADDRESS_V07 = - "0x0000000071727De22E5E9d8BAf0edAc6f37da032" as const; - // create clients -------------------------------------------- export const publicClient = createPublicClient({ transport: http(baseSepolia.rpcUrls.default.http[0]), }); -export const pimlicoPaymasterClient = createClient({ +export const paymasterClient = createClient({ chain: baseSepolia, transport: pimlicoTransport("v2"), }).extend(pimlicoPaymasterActions(ENTRYPOINT_ADDRESS_V07)); diff --git a/apps/wallet/src/pages/relay/deploy.ts b/apps/wallet/src/pages/relay/deploy.ts index 786ba72..721b573 100644 --- a/apps/wallet/src/pages/relay/deploy.ts +++ b/apps/wallet/src/pages/relay/deploy.ts @@ -76,9 +76,25 @@ const relaySafeDeploy = async ({ account: privateKeyToAccount(import.meta.env.PRIVATE_KEY), }); + // const { request, result } = await publicClient.simulateContract({ + // account: walletClient.account, + // address: V6_BUNDLER_ONIT_FACTORY_ADDRESS, + // abi: createSafe4337Abi, + // functionName: "createSafe4337", + // args: [publicKey, 0n], + // }); + + // const { request, result } = await publicClient.simulateContract({ + // account: walletClient.account, + // address: V6_BUNDLER_ONIT_FACTORY_ADDRESS, + // abi: createBasic4337Abi, + // functionName: "createBasic4337", + // args: [publicKey, 0n], + // }); + const { request, result } = await publicClient.simulateContract({ account: walletClient.account, - address: ONIT_FACTORY_ADDRESS, + address: V7_BUNDLER_SAFE_FACTORY_ADDRESS, abi: createSafe4337Abi, functionName: "createSafe4337", args: [publicKey, 0n], @@ -91,9 +107,18 @@ const relaySafeDeploy = async ({ // abis -------------------------------------------- -const ONIT_FACTORY_ADDRESS = +const V7_BUNDLER_SAFE_ONIT_FACTORY_ADDRESS = "0x42ab880ea77fc7a09eb6ba0fe82fbc9901c114b6" as const; +const V6_BUNDLER_SAFE_ONIT_FACTORY_ADDRESS = + "0xa4025cc96a042a4522F9115478D4d527F954a40E" as const; + +const V6_BUNDLER_ONIT_FACTORY_ADDRESS = + "0x70b3b72f7737c017888d64e60c5bfee6ca226b85" as const; + +const V7_BUNDLER_SAFE_FACTORY_ADDRESS = + "0x758f1ce181e74b4eb3d38441a0b2b117991c5cc8" as const; + const createSafe4337Abi = [ { type: "function", @@ -112,3 +137,22 @@ const createSafe4337Abi = [ stateMutability: "nonpayable", }, ] as const; + +const createBasic4337Abi = [ + { + type: "function", + name: "createBasic4337", + inputs: [ + { + name: "passkeyPublicKey", + type: "uint256[2]", + internalType: "uint256[2]", + }, + { name: "nonce", type: "uint256", internalType: "uint256" }, + ], + outputs: [ + { name: "onitAccountAddress", type: "address", internalType: "address" }, + ], + stateMutability: "nonpayable", + }, +] as const; diff --git a/packages/spc-lib/src/pay-with-spc.ts b/packages/spc-lib/src/pay-with-spc.ts index 08d1487..2b6f3e5 100644 --- a/packages/spc-lib/src/pay-with-spc.ts +++ b/packages/spc-lib/src/pay-with-spc.ts @@ -72,7 +72,7 @@ export const payWithSPC = async ( try { response = await request.show(); - console.log("spc response", response); + console.log("spc response", response, response.details); // response.details is a PublicKeyCredential, with a clientDataJSON that // contains the transaction data for verification by the issuing bank. @@ -86,7 +86,7 @@ export const payWithSPC = async ( response: { clientDataJSON: fromBuffer(cred.response.clientDataJSON), authenticatorData: fromBuffer(cred.response.authenticatorData), - signature: fromBuffer(cred.response.signature), + signature: fromBuffer(cred.response.signature, "hex"), userHandle: fromBuffer(cred.response.userHandle), }, }; @@ -94,6 +94,7 @@ export const payWithSPC = async ( await response.complete("success"); /* send response.details to the issuing bank for verification */ + return serialisableCredential; } catch (err) { await response.complete("fail"); /* SPC cannot be used; merchant should fallback to traditional flows */ From 496eaea82b17005eacfcac21c4109d8947112f1b Mon Sep 17 00:00:00 2001 From: Peter Ferguson Date: Sun, 17 Mar 2024 05:51:26 +0000 Subject: [PATCH 4/5] update copy --- apps/dapp/src/components/create-payment-button.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/apps/dapp/src/components/create-payment-button.tsx b/apps/dapp/src/components/create-payment-button.tsx index e554cdc..0e05dac 100644 --- a/apps/dapp/src/components/create-payment-button.tsx +++ b/apps/dapp/src/components/create-payment-button.tsx @@ -1,10 +1,6 @@ import { Button } from "@/components/ui/button"; import { formatMintCouponToModuleExecutionCalldata } from "@/lib/example-nft"; -import { - bundlerClient, - paymasterClient, - publicClient, -} from "@/lib/permissionless"; +import { bundlerClient, publicClient } from "@/lib/permissionless"; import { getPayeeOrigin, getPaymentOrigin } from "@/lib/utils"; import { ENTRYPOINT_ADDRESS_V07, @@ -50,7 +46,7 @@ export function CreatePaymentButton() { timeout: 60000, }, getPayeeOrigin(), - getPaymentDetails("0.0000001"), + getPaymentDetails("0.00"), address, ); @@ -62,7 +58,7 @@ export function CreatePaymentButton() { }); }} > - Create Payment + Claim Coupon ); } From c9631afb036c3d4dd0d4e94bf95679459107a814 Mon Sep 17 00:00:00 2001 From: Peter Ferguson Date: Sun, 17 Mar 2024 05:53:07 +0000 Subject: [PATCH 5/5] update button name --- .../{create-payment-button.tsx => claim-coupon-button.tsx} | 2 +- apps/dapp/src/components/nft-image-card.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename apps/dapp/src/components/{create-payment-button.tsx => claim-coupon-button.tsx} (98%) diff --git a/apps/dapp/src/components/create-payment-button.tsx b/apps/dapp/src/components/claim-coupon-button.tsx similarity index 98% rename from apps/dapp/src/components/create-payment-button.tsx rename to apps/dapp/src/components/claim-coupon-button.tsx index 0e05dac..42bc6cf 100644 --- a/apps/dapp/src/components/create-payment-button.tsx +++ b/apps/dapp/src/components/claim-coupon-button.tsx @@ -17,7 +17,7 @@ import { import type { Address } from "viem"; import { baseSepolia } from "viem/chains"; -export function CreatePaymentButton() { +export function ClaimCouponButton() { return (