From 48430a7db43c8c56fd8c7b02b07fcd584145478e Mon Sep 17 00:00:00 2001 From: Erik Nucibella Date: Sun, 24 Sep 2023 06:16:38 -0400 Subject: [PATCH 01/12] feat(LensUtils): getProfilePicture function --- src/utils/LensUtils/LensUtils.ts | 15 +++++++++++++++ src/utils/LensUtils/index.ts | 2 ++ src/utils/index.ts | 3 ++- 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 src/utils/LensUtils/LensUtils.ts create mode 100644 src/utils/LensUtils/index.ts diff --git a/src/utils/LensUtils/LensUtils.ts b/src/utils/LensUtils/LensUtils.ts new file mode 100644 index 0000000..4ebbecc --- /dev/null +++ b/src/utils/LensUtils/LensUtils.ts @@ -0,0 +1,15 @@ +import { DEFUALT_USER_IMG_PLACEHOLDER } from "@/constants"; +import { Profile } from "@lens-protocol/react-web"; +import { FormattingUtils } from ".."; + +export const getProfilePicture = (profile?: Profile) => { + if (!profile) return DEFUALT_USER_IMG_PLACEHOLDER; + if (profile.picture?.__typename === "MediaSet") { + const ipfsUrl = FormattingUtils.ipfsUriToHttps( + profile.picture.original.url + ); + return ipfsUrl ?? profile.picture.original.url; + } else { + return DEFUALT_USER_IMG_PLACEHOLDER; + } +}; diff --git a/src/utils/LensUtils/index.ts b/src/utils/LensUtils/index.ts new file mode 100644 index 0000000..1f532bf --- /dev/null +++ b/src/utils/LensUtils/index.ts @@ -0,0 +1,2 @@ +import * as LensUtils from "./LensUtils"; +export default LensUtils; diff --git a/src/utils/index.ts b/src/utils/index.ts index 7053ad9..873d39a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,3 @@ import FormattingUtils from "./FormattingUtils"; -export { FormattingUtils }; +import LensUtils from "./LensUtils"; +export { FormattingUtils, LensUtils }; From d0f45c111645226d2c8bea33865c871345c58ffe Mon Sep 17 00:00:00 2001 From: Erik Nucibella Date: Sun, 24 Sep 2023 06:17:22 -0400 Subject: [PATCH 02/12] feat(networking): methods for contract interactions --- src/networking/index.ts | 3 + src/networking/transactions/index.ts | 3 + src/networking/transactions/transactions.ts | 112 ++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 src/networking/transactions/index.ts create mode 100644 src/networking/transactions/transactions.ts diff --git a/src/networking/index.ts b/src/networking/index.ts index a5a202e..e350483 100644 --- a/src/networking/index.ts +++ b/src/networking/index.ts @@ -3,3 +3,6 @@ export * from "./biconomy"; export * from "./prices"; export * from "./RubyRingAPI"; export * from "./Web3Storage"; + +import Transactions from "./transactions"; +export { Transactions }; diff --git a/src/networking/transactions/index.ts b/src/networking/transactions/index.ts new file mode 100644 index 0000000..3cd63fa --- /dev/null +++ b/src/networking/transactions/index.ts @@ -0,0 +1,3 @@ +import * as Transactions from "./transactions"; + +export default Transactions; diff --git a/src/networking/transactions/transactions.ts b/src/networking/transactions/transactions.ts new file mode 100644 index 0000000..515630a --- /dev/null +++ b/src/networking/transactions/transactions.ts @@ -0,0 +1,112 @@ +import { BigNumber } from "ethers"; +import { BiconomySmartAccount } from "@biconomy/account"; +import { config } from "@/config"; +import { CONTRACT } from "@/provider"; + +//eslint-disable-next-line +const sendUserOp = async ( + //eslint-disable-next-line + userOp: any, + biconomySmartAccount: BiconomySmartAccount +): Promise => { + try { + const userOpResponse = await biconomySmartAccount.sendUserOp(userOp); + + console.log(`userOp Hash: ${userOpResponse.userOpHash}`); + + const transactionDetails = await userOpResponse.wait(); + + console.log( + `transactionDetails: ${JSON.stringify(transactionDetails, null, "\t")}` + ); + } catch (e) { + console.log("error received ", e); + } +}; + +export const buyGems = async ( + gemsSubject: string, + amount: number, + ethCost: BigNumber, + smartAccount: BiconomySmartAccount +): Promise => { + try { + // Create the calldata for our UserOperation + const buyGemsTx = await CONTRACT.populateTransaction.buyGems( + gemsSubject, + amount + ); + + const calldata = buyGemsTx.data; + + // Build the UserOperation + const userOp = await smartAccount.buildUserOp([ + { + to: config.contractAddress, + data: calldata, + value: ethCost + } + ]); + + const userOpEstimated = await smartAccount.estimateUserOpGas(userOp); + + console.log( + `userOpEstimated: ${JSON.stringify(userOpEstimated, null, "\t")}` + ); + + // Send the UserOperation + const userOpResponse = await smartAccount.sendUserOp(userOpEstimated); + const receipt = await userOpResponse.wait(); + + console.log(`Transaction receipt: ${JSON.stringify(receipt, null, 2)}`); + + return receipt.receipt.transactionHash; + } catch (e) { + console.error("error received while sending userOp", e); + throw e; + } +}; + +//TODO: extract duplicated code +export const sellGems = async ( + gemsSubject: string, + amount: number, + ethCost: BigNumber, + smartAccount: BiconomySmartAccount +): Promise => { + try { + // Create the calldata for our UserOperation + const sellGemsTx = await CONTRACT.populateTransaction.sellGems( + gemsSubject, + amount + ); + + const calldata = sellGemsTx.data; + + // Build the UserOperation + const userOp = await smartAccount.buildUserOp([ + { + to: config.contractAddress, + data: calldata, + value: ethCost + } + ]); + + const userOpEstimated = await smartAccount.estimateUserOpGas(userOp); + + console.log( + `userOpEstimated: ${JSON.stringify(userOpEstimated, null, "\t")}` + ); + + // Send the UserOperation + const userOpResponse = await smartAccount.sendUserOp(userOpEstimated); + const receipt = await userOpResponse.wait(); + + console.log(`Transaction receipt: ${JSON.stringify(receipt, null, 2)}`); + + return receipt.receipt.transactionHash; + } catch (e) { + console.error("error received while sending userOp", e); + throw e; + } +}; From 34929b19d96527cb9ca9bbb968be4d5ad1cef495 Mon Sep 17 00:00:00 2001 From: Erik Nucibella Date: Sun, 24 Sep 2023 06:18:22 -0400 Subject: [PATCH 03/12] feat(lens): publications components --- src/components/Lens/CreatePost/CreatePost.tsx | 220 ++++++++++++++++++ src/components/Lens/CreatePost/index.ts | 1 + .../Lens/Publications/LensPublication.tsx | 29 +++ .../Lens/Publications/Publications.tsx | 51 ++++ src/components/Lens/Publications/index.ts | 2 + 5 files changed, 303 insertions(+) create mode 100644 src/components/Lens/CreatePost/CreatePost.tsx create mode 100644 src/components/Lens/CreatePost/index.ts create mode 100644 src/components/Lens/Publications/LensPublication.tsx create mode 100644 src/components/Lens/Publications/Publications.tsx create mode 100644 src/components/Lens/Publications/index.ts diff --git a/src/components/Lens/CreatePost/CreatePost.tsx b/src/components/Lens/CreatePost/CreatePost.tsx new file mode 100644 index 0000000..251252b --- /dev/null +++ b/src/components/Lens/CreatePost/CreatePost.tsx @@ -0,0 +1,220 @@ +import { web3StorageClient } from "@/networking"; +import { useAccountStore } from "@/store"; +import { ContentFocus, useCreatePost } from "@lens-protocol/react-web"; +import { ChangeEvent, useCallback, useMemo, useState } from "react"; +import { getWeb3StorageLink } from "../CreateLensProfileForm/helpers"; +import toast from "react-hot-toast"; +import { FaGem } from "react-icons/fa"; +import { BeatLoader } from "react-spinners"; + +type Props = { + refetchPublications: () => void; +}; + +function classNames(...classes: string[]) { + return classes.filter(Boolean).join(" "); +} + +export default function CreatePost({ refetchPublications }: Props) { + const { lensAccount } = useAccountStore(); + + const [postText, setPostText] = useState(""); + + const [loading, setLoading] = useState(false); + + const upload = useCallback(async (data: unknown): Promise => { + const serialized = JSON.stringify(data); + + const blob = new Blob([serialized], { type: "application/json" }); + + const name = blob.name; + + const cid = await web3StorageClient.put([new File([blob], name)]); + + const web3StorageIPFSurl = getWeb3StorageLink(cid); + + const url = `${web3StorageIPFSurl}/${name}`; + + return url; + }, []); + + // TODO: Add error handling and loading state + const { + execute: createPost + // error, + // isPending + } = useCreatePost({ + publisher: lensAccount!, + upload + }); + + const handleOnTextChange = useCallback( + (event: ChangeEvent) => { + const value = event.target.value; + + setPostText(value); + }, + [] + ); + + const onPostCreate = useCallback(async () => { + try { + setLoading(true); + + await createPost({ + content: postText, + contentFocus: ContentFocus.TEXT_ONLY, + locale: "en" + }); + + refetchPublications(); + + toast.success("Post published to your Ring"); + setLoading(false); + + setPostText(""); + } catch (error) { + console.error(error); + setLoading(false); + + toast.error("Oops! Something went wrong, try again."); + } + }, [createPost, postText, refetchPublications]); + + const isPostDisabled = useMemo(() => { + return postText.length === 0; + }, [postText]); + + return ( + <> +
+
+
+
+ + + + + +
+
+
+
+