diff --git a/apps/dapp/package.json b/apps/dapp/package.json index 7167869..ccd9c39 100644 --- a/apps/dapp/package.json +++ b/apps/dapp/package.json @@ -26,10 +26,12 @@ "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", "typescript": "^5.4.2", + "usehooks-ts": "^3.0.1", "viem": "^2.8.10" } } \ No newline at end of file diff --git a/apps/dapp/src/components/claim-coupon-button.tsx b/apps/dapp/src/components/claim-coupon-button.tsx new file mode 100644 index 0000000..42bc6cf --- /dev/null +++ b/apps/dapp/src/components/claim-coupon-button.tsx @@ -0,0 +1,124 @@ +import { Button } from "@/components/ui/button"; +import { formatMintCouponToModuleExecutionCalldata } from "@/lib/example-nft"; +import { bundlerClient, publicClient } from "@/lib/permissionless"; +import { getPayeeOrigin, getPaymentOrigin } from "@/lib/utils"; +import { + ENTRYPOINT_ADDRESS_V07, + getAccountNonce, + getUserOperationHash, + type UserOperation, +} from "permissionless"; +import { + fallbackToIframeCredentialCreation, + getAvailableCredentials, + getPaymentDetails, + payWithSPC, +} from "spc-lib"; +import type { Address } from "viem"; +import { baseSepolia } from "viem/chains"; + +export function ClaimCouponButton() { + return ( + { + e.preventDefault(); + + // - allow only a single credential for now + const [credential] = + (await getAvailableCredentials(getPaymentOrigin())) ?? []; + + const address = credential?.address as Address | undefined; + if (!address) { + return fallbackToIframeCredentialCreation(); + } + + const { userOperation, userOperationHash } = + await getMintNftUserOperation(address); + + const { + response: { signature }, + } = await payWithSPC( + { + rpId: getPaymentOrigin().replace("https://", ""), + allowedCredentials: [credential.credentialId], + challenge: userOperationHash, + timeout: 60000, + }, + getPayeeOrigin(), + getPaymentDetails("0.00"), + address, + ); + + await bundlerClient.sendUserOperation({ + userOperation: { + ...userOperation, + signature: `0x${signature}`, + }, + }); + }} + > + Claim Coupon + + ); +} + +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/components/create-payment-button.tsx b/apps/dapp/src/components/create-payment-button.tsx deleted file mode 100644 index d03520d..0000000 --- a/apps/dapp/src/components/create-payment-button.tsx +++ /dev/null @@ -1,126 +0,0 @@ -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(); -}; - -export function CreatePaymentButton() { - return ( - { - e.preventDefault(); - - const allowedCredentials = await getAvailableCredentials(); - - console.log("allowedCredentials", allowedCredentials); - - if (!allowedCredentials || allowedCredentials.length === 0) { - return fallbackToIframeCredentialCreation(); - } - - await payWithSPC( - { - allowedCredentials, - challenge: "challenge", - timeout: 60000, - }, - getPaymentDetails("0.0000001"), - "0x123...456", - ); - }} - > - Create Payment - - ); -} diff --git a/apps/dapp/src/components/nft-image-card.tsx b/apps/dapp/src/components/nft-image-card.tsx index 9e20007..700395a 100644 --- a/apps/dapp/src/components/nft-image-card.tsx +++ b/apps/dapp/src/components/nft-image-card.tsx @@ -1,34 +1,48 @@ -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 { ClaimCouponButton } from "./claim-coupon-button"; +import { useLocalStorage } from "usehooks-ts"; +import type { Address } from "viem"; export function NftImageCard() { - const tokenId = 1n; const [uri, setUri] = React.useState(null); const [hasClaimed, setHasClaimed] = React.useState(false); + const [address] = useLocalStorage("address", undefined); React.useEffect(() => { - getNftUri(0).then((r) => setUri(r)); + getNftUri(0n).then((r) => setUri(r)); }, []); React.useEffect(() => { - getBalance('0x3896a2938d345d3A351cE152AF1b8Cb17bb006be', 0n).then((r) => { - console.log({ r }); - setHasClaimed(r > 0) - }); - }, []); + getBalance(address, 0n).then((r) => setHasClaimed(r > 0)); + }, [address]); return ( 10 Eth Coupon - Claim your free 10 eth coupon which can + + Claim your free 10 eth coupon which can + - - + + {uri && } - - 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}`}>
You have {hasClaimed ? '' : 'not'} claimed
You have claimed