├── .eslintrc.json ├── .gitignore ├── README.md ├── components ├── Header │ ├── Header.module.css │ └── Header.tsx └── Publication │ ├── Publication.module.css │ └── PublicationCard.tsx ├── const └── abis.ts ├── graphql ├── auth │ ├── generateChallenge.ts │ ├── getAccessToken.ts │ └── refreshAccessToken.ts ├── initClient.ts ├── mutate │ └── followUser.ts └── query │ ├── doesFollowUser.ts │ ├── getProfile.ts │ ├── getProfileByAddress.ts │ ├── getPublications.ts │ └── mostFollowedProfiles.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── index.tsx └── profile │ └── [handle].tsx ├── public ├── favicon.ico ├── lens.jpeg ├── thirdweb.svg └── vercel.svg ├── styles ├── Home.module.css ├── Profile.module.css └── globals.css ├── tsconfig.json ├── types ├── Profile.ts └── Publication.ts └── util ├── ethers.service.ts ├── helpers.ts ├── login.ts ├── parseJwt.ts └── useLensUser.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!Important] 2 | > This repository is referencing the `mumbai` chain. 3 | > 4 | > `Mumbai` [is deprecated since 08/04/2024](https://blog.thirdweb.com/deprecation-of-mumbai-testnet/), meaning the code in this repository will no longer work out of the box. 5 | > 6 | > You can still use this repository, however you will have to switch any references to `mumbai` to another chain. 7 | 8 | # Lens + thirdweb Starter 9 | 10 | A production-ready starter kit for building apps on top of [Lens](https://docs.lens.xyz/docs), featuring: 11 | 12 | - **[Next.js](https://nextjs.org/)**: Epic React framework for building production-ready apps. 13 | - **[TypeScript](https://www.typescriptlang.org/)**: Type-safety for writing less buggy code. 14 | - **[GraphQL](https://graphql.org/)** and **[urql](https://formidable.com/open-source/urql/)**: Query data from Lens with GraphQL. 15 | - **[React Query](https://react-query.tanstack.com/)**: Utility for fetching, caching and updating data from Lens. 16 | -------------------------------------------------------------------------------- /components/Header/Header.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 64px; 3 | padding-top: 8px; 4 | padding-bottom: 8px; 5 | padding-left: 1em; 6 | padding-right: 1em; 7 | width: 100vw; 8 | display: flex; 9 | flex-direction: row; 10 | align-items: center; 11 | justify-content: space-between; 12 | border-bottom: 1px solid rgba(255, 255, 255, 0.1); 13 | backdrop-filter: blur(10px); 14 | gap: 2%; 15 | position: fixed; 16 | } 17 | 18 | .homeNavigator { 19 | display: flex; 20 | flex-direction: row; 21 | align-items: center; 22 | justify-content: center; 23 | gap: 12px; 24 | text-decoration: none; 25 | } 26 | 27 | .logo { 28 | height: 36px; 29 | width: 36px; 30 | } 31 | 32 | .logoText { 33 | color: #fff; 34 | font-size: 1.25rem; 35 | } 36 | 37 | .profileName { 38 | margin-right: 8px; 39 | } 40 | 41 | .signInButton { 42 | background-color: var(--tw-color1); 43 | border-radius: 8px; 44 | color: white; 45 | font-weight: 600; 46 | font-size: 1rem; 47 | padding: 8px 16px; 48 | border: none; 49 | margin-right: 12px; 50 | } 51 | 52 | .signInButton:hover { 53 | background-color: var(--tw-color1-hover); 54 | cursor: pointer; 55 | transition: 0.6s; 56 | } 57 | -------------------------------------------------------------------------------- /components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChainId, 3 | ConnectWallet, 4 | useAddress, 5 | useNetwork, 6 | useNetworkMismatch, 7 | useSDK, 8 | } from "@thirdweb-dev/react"; 9 | import Link from "next/link"; 10 | import styles from "./Header.module.css"; 11 | import useLensUser from "../../util/useLensUser"; 12 | import login from "../../util/login"; 13 | 14 | export default function Header() { 15 | const sdk = useSDK(); 16 | const address = useAddress(); 17 | const isWrongNetwork = useNetworkMismatch(); 18 | const [, switchNetwork] = useNetwork(); 19 | const { isSignedIn, setIsSignedIn, loadingSignIn, profile, loadingProfile } = 20 | useLensUser(); 21 | 22 | async function signIn() { 23 | if (!address || !sdk) return; 24 | 25 | if (isWrongNetwork) { 26 | switchNetwork?.(ChainId.Polygon); 27 | return; 28 | } 29 | 30 | await login(address, sdk); 31 | setIsSignedIn(true); 32 | } 33 | 34 | return ( 35 |
36 | 37 | Lens Logo 38 |

Lens Starter Kit

39 | 40 | 41 | 42 |
43 | ); 44 | 45 | // Separate component for what to show on right side 46 | function RightSide() { 47 | // Connect Wallet First 48 | if (!address) { 49 | return ( 50 |
51 | 52 |
53 | ); 54 | } 55 | 56 | // Loading sign in state 57 | if (loadingSignIn) { 58 | return
Loading...
; 59 | } 60 | 61 | // Not signed in 62 | if (!isSignedIn) { 63 | return ( 64 | 67 | ); 68 | } 69 | 70 | // Loading profile 71 | if (loadingProfile) { 72 | return
Loading...
; 73 | } 74 | 75 | // Is signed in but doesn't have profile 76 | if (!profile) { 77 | return

No Lens profile.

; 78 | } 79 | 80 | // Is signed in and has profile 81 | return

@{profile.handle}

; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /components/Publication/Publication.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: flex-start; 5 | align-items: flex-start; 6 | gap: 16px; 7 | outline: 1px solid grey; 8 | padding: 8px; 9 | border-radius: 16px; 10 | width: 100%; 11 | max-width: 620px; 12 | min-height: 96px; 13 | } 14 | 15 | .textContainer { 16 | display: flex; 17 | flex-direction: column; 18 | align-items: flex-start; 19 | text-align: left; 20 | margin-left: 8px; 21 | overflow-x: hidden; 22 | word-break: break-all; 23 | } 24 | 25 | .title { 26 | margin: 0px; 27 | padding: 0px; 28 | font-size: 1.3rem; 29 | } 30 | 31 | .content { 32 | margin: 0px; 33 | padding: 0px; 34 | text-overflow: ellipsis; 35 | /* If word does not fit parent, break */ 36 | } 37 | 38 | .placeholder { 39 | width: 100%; 40 | height: 100%; 41 | background-color: grey; 42 | border-radius: 16px; 43 | display: flex; 44 | align-items: center; 45 | justify-content: center; 46 | } 47 | 48 | .placeholderText { 49 | font-size: 0.7rem; 50 | color: white; 51 | margin: 0px; 52 | } 53 | -------------------------------------------------------------------------------- /components/Publication/PublicationCard.tsx: -------------------------------------------------------------------------------- 1 | import { MediaRenderer } from "@thirdweb-dev/react"; 2 | import Publication from "../../types/Publication"; 3 | import styles from "./Publication.module.css"; 4 | 5 | type Props = { 6 | publication: Publication; 7 | }; 8 | 9 | export default function PublicationCard({ publication }: Props) { 10 | return ( 11 |
12 |
13 |

{publication.metadata.name}

14 |

{publication.metadata.content}

15 |
16 | {publication.metadata.image && ( 17 | 27 | )} 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /const/abis.ts: -------------------------------------------------------------------------------- 1 | // Learn how we use the thirdweb SDK to connect to the Lens smart contract using it's ABI: 2 | // https://blog.thirdweb.com/guides/how-to-use-any-smart-contract-with-thirdweb-sdk-using-abi/ 3 | export const LENS_PROTOCOL_PROFILES_ABI = [ 4 | { 5 | inputs: [ 6 | { internalType: "address", name: "followNFTImpl", type: "address" }, 7 | { internalType: "address", name: "collectNFTImpl", type: "address" }, 8 | ], 9 | stateMutability: "nonpayable", 10 | type: "constructor", 11 | }, 12 | { inputs: [], name: "CallerNotCollectNFT", type: "error" }, 13 | { inputs: [], name: "CallerNotFollowNFT", type: "error" }, 14 | { inputs: [], name: "EmergencyAdminCannotUnpause", type: "error" }, 15 | { inputs: [], name: "InitParamsInvalid", type: "error" }, 16 | { inputs: [], name: "Initialized", type: "error" }, 17 | { inputs: [], name: "NotGovernance", type: "error" }, 18 | { inputs: [], name: "NotGovernanceOrEmergencyAdmin", type: "error" }, 19 | { inputs: [], name: "NotOwnerOrApproved", type: "error" }, 20 | { inputs: [], name: "NotProfileOwner", type: "error" }, 21 | { inputs: [], name: "NotProfileOwnerOrDispatcher", type: "error" }, 22 | { inputs: [], name: "Paused", type: "error" }, 23 | { inputs: [], name: "ProfileCreatorNotWhitelisted", type: "error" }, 24 | { inputs: [], name: "ProfileImageURILengthInvalid", type: "error" }, 25 | { inputs: [], name: "PublicationDoesNotExist", type: "error" }, 26 | { inputs: [], name: "PublishingPaused", type: "error" }, 27 | { inputs: [], name: "SignatureExpired", type: "error" }, 28 | { inputs: [], name: "SignatureInvalid", type: "error" }, 29 | { inputs: [], name: "ZeroSpender", type: "error" }, 30 | { 31 | anonymous: false, 32 | inputs: [ 33 | { 34 | indexed: true, 35 | internalType: "address", 36 | name: "owner", 37 | type: "address", 38 | }, 39 | { 40 | indexed: true, 41 | internalType: "address", 42 | name: "approved", 43 | type: "address", 44 | }, 45 | { 46 | indexed: true, 47 | internalType: "uint256", 48 | name: "tokenId", 49 | type: "uint256", 50 | }, 51 | ], 52 | name: "Approval", 53 | type: "event", 54 | }, 55 | { 56 | anonymous: false, 57 | inputs: [ 58 | { 59 | indexed: true, 60 | internalType: "address", 61 | name: "owner", 62 | type: "address", 63 | }, 64 | { 65 | indexed: true, 66 | internalType: "address", 67 | name: "operator", 68 | type: "address", 69 | }, 70 | { indexed: false, internalType: "bool", name: "approved", type: "bool" }, 71 | ], 72 | name: "ApprovalForAll", 73 | type: "event", 74 | }, 75 | { 76 | anonymous: false, 77 | inputs: [ 78 | { indexed: true, internalType: "address", name: "from", type: "address" }, 79 | { indexed: true, internalType: "address", name: "to", type: "address" }, 80 | { 81 | indexed: true, 82 | internalType: "uint256", 83 | name: "tokenId", 84 | type: "uint256", 85 | }, 86 | ], 87 | name: "Transfer", 88 | type: "event", 89 | }, 90 | { 91 | inputs: [ 92 | { internalType: "address", name: "to", type: "address" }, 93 | { internalType: "uint256", name: "tokenId", type: "uint256" }, 94 | ], 95 | name: "approve", 96 | outputs: [], 97 | stateMutability: "nonpayable", 98 | type: "function", 99 | }, 100 | { 101 | inputs: [{ internalType: "address", name: "owner", type: "address" }], 102 | name: "balanceOf", 103 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 104 | stateMutability: "view", 105 | type: "function", 106 | }, 107 | { 108 | inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], 109 | name: "burn", 110 | outputs: [], 111 | stateMutability: "nonpayable", 112 | type: "function", 113 | }, 114 | { 115 | inputs: [ 116 | { internalType: "uint256", name: "tokenId", type: "uint256" }, 117 | { 118 | components: [ 119 | { internalType: "uint8", name: "v", type: "uint8" }, 120 | { internalType: "bytes32", name: "r", type: "bytes32" }, 121 | { internalType: "bytes32", name: "s", type: "bytes32" }, 122 | { internalType: "uint256", name: "deadline", type: "uint256" }, 123 | ], 124 | internalType: "struct DataTypes.EIP712Signature", 125 | name: "sig", 126 | type: "tuple", 127 | }, 128 | ], 129 | name: "burnWithSig", 130 | outputs: [], 131 | stateMutability: "nonpayable", 132 | type: "function", 133 | }, 134 | { 135 | inputs: [ 136 | { internalType: "uint256", name: "profileId", type: "uint256" }, 137 | { internalType: "uint256", name: "pubId", type: "uint256" }, 138 | { internalType: "bytes", name: "data", type: "bytes" }, 139 | ], 140 | name: "collect", 141 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 142 | stateMutability: "nonpayable", 143 | type: "function", 144 | }, 145 | { 146 | inputs: [ 147 | { 148 | components: [ 149 | { internalType: "address", name: "collector", type: "address" }, 150 | { internalType: "uint256", name: "profileId", type: "uint256" }, 151 | { internalType: "uint256", name: "pubId", type: "uint256" }, 152 | { internalType: "bytes", name: "data", type: "bytes" }, 153 | { 154 | components: [ 155 | { internalType: "uint8", name: "v", type: "uint8" }, 156 | { internalType: "bytes32", name: "r", type: "bytes32" }, 157 | { internalType: "bytes32", name: "s", type: "bytes32" }, 158 | { internalType: "uint256", name: "deadline", type: "uint256" }, 159 | ], 160 | internalType: "struct DataTypes.EIP712Signature", 161 | name: "sig", 162 | type: "tuple", 163 | }, 164 | ], 165 | internalType: "struct DataTypes.CollectWithSigData", 166 | name: "vars", 167 | type: "tuple", 168 | }, 169 | ], 170 | name: "collectWithSig", 171 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 172 | stateMutability: "nonpayable", 173 | type: "function", 174 | }, 175 | { 176 | inputs: [ 177 | { 178 | components: [ 179 | { internalType: "uint256", name: "profileId", type: "uint256" }, 180 | { internalType: "string", name: "contentURI", type: "string" }, 181 | { 182 | internalType: "uint256", 183 | name: "profileIdPointed", 184 | type: "uint256", 185 | }, 186 | { internalType: "uint256", name: "pubIdPointed", type: "uint256" }, 187 | { internalType: "bytes", name: "referenceModuleData", type: "bytes" }, 188 | { internalType: "address", name: "collectModule", type: "address" }, 189 | { 190 | internalType: "bytes", 191 | name: "collectModuleInitData", 192 | type: "bytes", 193 | }, 194 | { internalType: "address", name: "referenceModule", type: "address" }, 195 | { 196 | internalType: "bytes", 197 | name: "referenceModuleInitData", 198 | type: "bytes", 199 | }, 200 | ], 201 | internalType: "struct DataTypes.CommentData", 202 | name: "vars", 203 | type: "tuple", 204 | }, 205 | ], 206 | name: "comment", 207 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 208 | stateMutability: "nonpayable", 209 | type: "function", 210 | }, 211 | { 212 | inputs: [ 213 | { 214 | components: [ 215 | { internalType: "uint256", name: "profileId", type: "uint256" }, 216 | { internalType: "string", name: "contentURI", type: "string" }, 217 | { 218 | internalType: "uint256", 219 | name: "profileIdPointed", 220 | type: "uint256", 221 | }, 222 | { internalType: "uint256", name: "pubIdPointed", type: "uint256" }, 223 | { internalType: "bytes", name: "referenceModuleData", type: "bytes" }, 224 | { internalType: "address", name: "collectModule", type: "address" }, 225 | { 226 | internalType: "bytes", 227 | name: "collectModuleInitData", 228 | type: "bytes", 229 | }, 230 | { internalType: "address", name: "referenceModule", type: "address" }, 231 | { 232 | internalType: "bytes", 233 | name: "referenceModuleInitData", 234 | type: "bytes", 235 | }, 236 | { 237 | components: [ 238 | { internalType: "uint8", name: "v", type: "uint8" }, 239 | { internalType: "bytes32", name: "r", type: "bytes32" }, 240 | { internalType: "bytes32", name: "s", type: "bytes32" }, 241 | { internalType: "uint256", name: "deadline", type: "uint256" }, 242 | ], 243 | internalType: "struct DataTypes.EIP712Signature", 244 | name: "sig", 245 | type: "tuple", 246 | }, 247 | ], 248 | internalType: "struct DataTypes.CommentWithSigData", 249 | name: "vars", 250 | type: "tuple", 251 | }, 252 | ], 253 | name: "commentWithSig", 254 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 255 | stateMutability: "nonpayable", 256 | type: "function", 257 | }, 258 | { 259 | inputs: [ 260 | { 261 | components: [ 262 | { internalType: "address", name: "to", type: "address" }, 263 | { internalType: "string", name: "handle", type: "string" }, 264 | { internalType: "string", name: "imageURI", type: "string" }, 265 | { internalType: "address", name: "followModule", type: "address" }, 266 | { 267 | internalType: "bytes", 268 | name: "followModuleInitData", 269 | type: "bytes", 270 | }, 271 | { internalType: "string", name: "followNFTURI", type: "string" }, 272 | ], 273 | internalType: "struct DataTypes.CreateProfileData", 274 | name: "vars", 275 | type: "tuple", 276 | }, 277 | ], 278 | name: "createProfile", 279 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 280 | stateMutability: "nonpayable", 281 | type: "function", 282 | }, 283 | { 284 | inputs: [{ internalType: "address", name: "wallet", type: "address" }], 285 | name: "defaultProfile", 286 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 287 | stateMutability: "view", 288 | type: "function", 289 | }, 290 | { 291 | inputs: [ 292 | { internalType: "uint256", name: "profileId", type: "uint256" }, 293 | { internalType: "uint256", name: "pubId", type: "uint256" }, 294 | { internalType: "uint256", name: "collectNFTId", type: "uint256" }, 295 | { internalType: "address", name: "from", type: "address" }, 296 | { internalType: "address", name: "to", type: "address" }, 297 | ], 298 | name: "emitCollectNFTTransferEvent", 299 | outputs: [], 300 | stateMutability: "nonpayable", 301 | type: "function", 302 | }, 303 | { 304 | inputs: [ 305 | { internalType: "uint256", name: "profileId", type: "uint256" }, 306 | { internalType: "uint256", name: "followNFTId", type: "uint256" }, 307 | { internalType: "address", name: "from", type: "address" }, 308 | { internalType: "address", name: "to", type: "address" }, 309 | ], 310 | name: "emitFollowNFTTransferEvent", 311 | outputs: [], 312 | stateMutability: "nonpayable", 313 | type: "function", 314 | }, 315 | { 316 | inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], 317 | name: "exists", 318 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 319 | stateMutability: "view", 320 | type: "function", 321 | }, 322 | { 323 | inputs: [ 324 | { internalType: "uint256[]", name: "profileIds", type: "uint256[]" }, 325 | { internalType: "bytes[]", name: "datas", type: "bytes[]" }, 326 | ], 327 | name: "follow", 328 | outputs: [{ internalType: "uint256[]", name: "", type: "uint256[]" }], 329 | stateMutability: "nonpayable", 330 | type: "function", 331 | }, 332 | { 333 | inputs: [ 334 | { 335 | components: [ 336 | { internalType: "address", name: "follower", type: "address" }, 337 | { internalType: "uint256[]", name: "profileIds", type: "uint256[]" }, 338 | { internalType: "bytes[]", name: "datas", type: "bytes[]" }, 339 | { 340 | components: [ 341 | { internalType: "uint8", name: "v", type: "uint8" }, 342 | { internalType: "bytes32", name: "r", type: "bytes32" }, 343 | { internalType: "bytes32", name: "s", type: "bytes32" }, 344 | { internalType: "uint256", name: "deadline", type: "uint256" }, 345 | ], 346 | internalType: "struct DataTypes.EIP712Signature", 347 | name: "sig", 348 | type: "tuple", 349 | }, 350 | ], 351 | internalType: "struct DataTypes.FollowWithSigData", 352 | name: "vars", 353 | type: "tuple", 354 | }, 355 | ], 356 | name: "followWithSig", 357 | outputs: [{ internalType: "uint256[]", name: "", type: "uint256[]" }], 358 | stateMutability: "nonpayable", 359 | type: "function", 360 | }, 361 | { 362 | inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], 363 | name: "getApproved", 364 | outputs: [{ internalType: "address", name: "", type: "address" }], 365 | stateMutability: "view", 366 | type: "function", 367 | }, 368 | { 369 | inputs: [ 370 | { internalType: "uint256", name: "profileId", type: "uint256" }, 371 | { internalType: "uint256", name: "pubId", type: "uint256" }, 372 | ], 373 | name: "getCollectModule", 374 | outputs: [{ internalType: "address", name: "", type: "address" }], 375 | stateMutability: "view", 376 | type: "function", 377 | }, 378 | { 379 | inputs: [ 380 | { internalType: "uint256", name: "profileId", type: "uint256" }, 381 | { internalType: "uint256", name: "pubId", type: "uint256" }, 382 | ], 383 | name: "getCollectNFT", 384 | outputs: [{ internalType: "address", name: "", type: "address" }], 385 | stateMutability: "view", 386 | type: "function", 387 | }, 388 | { 389 | inputs: [], 390 | name: "getCollectNFTImpl", 391 | outputs: [{ internalType: "address", name: "", type: "address" }], 392 | stateMutability: "view", 393 | type: "function", 394 | }, 395 | { 396 | inputs: [ 397 | { internalType: "uint256", name: "profileId", type: "uint256" }, 398 | { internalType: "uint256", name: "pubId", type: "uint256" }, 399 | ], 400 | name: "getContentURI", 401 | outputs: [{ internalType: "string", name: "", type: "string" }], 402 | stateMutability: "view", 403 | type: "function", 404 | }, 405 | { 406 | inputs: [{ internalType: "uint256", name: "profileId", type: "uint256" }], 407 | name: "getDispatcher", 408 | outputs: [{ internalType: "address", name: "", type: "address" }], 409 | stateMutability: "view", 410 | type: "function", 411 | }, 412 | { 413 | inputs: [], 414 | name: "getDomainSeparator", 415 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 416 | stateMutability: "view", 417 | type: "function", 418 | }, 419 | { 420 | inputs: [{ internalType: "uint256", name: "profileId", type: "uint256" }], 421 | name: "getFollowModule", 422 | outputs: [{ internalType: "address", name: "", type: "address" }], 423 | stateMutability: "view", 424 | type: "function", 425 | }, 426 | { 427 | inputs: [{ internalType: "uint256", name: "profileId", type: "uint256" }], 428 | name: "getFollowNFT", 429 | outputs: [{ internalType: "address", name: "", type: "address" }], 430 | stateMutability: "view", 431 | type: "function", 432 | }, 433 | { 434 | inputs: [], 435 | name: "getFollowNFTImpl", 436 | outputs: [{ internalType: "address", name: "", type: "address" }], 437 | stateMutability: "view", 438 | type: "function", 439 | }, 440 | { 441 | inputs: [{ internalType: "uint256", name: "profileId", type: "uint256" }], 442 | name: "getFollowNFTURI", 443 | outputs: [{ internalType: "string", name: "", type: "string" }], 444 | stateMutability: "view", 445 | type: "function", 446 | }, 447 | { 448 | inputs: [], 449 | name: "getGovernance", 450 | outputs: [{ internalType: "address", name: "", type: "address" }], 451 | stateMutability: "view", 452 | type: "function", 453 | }, 454 | { 455 | inputs: [{ internalType: "uint256", name: "profileId", type: "uint256" }], 456 | name: "getHandle", 457 | outputs: [{ internalType: "string", name: "", type: "string" }], 458 | stateMutability: "view", 459 | type: "function", 460 | }, 461 | { 462 | inputs: [{ internalType: "uint256", name: "profileId", type: "uint256" }], 463 | name: "getProfile", 464 | outputs: [ 465 | { 466 | components: [ 467 | { internalType: "uint256", name: "pubCount", type: "uint256" }, 468 | { internalType: "address", name: "followModule", type: "address" }, 469 | { internalType: "address", name: "followNFT", type: "address" }, 470 | { internalType: "string", name: "handle", type: "string" }, 471 | { internalType: "string", name: "imageURI", type: "string" }, 472 | { internalType: "string", name: "followNFTURI", type: "string" }, 473 | ], 474 | internalType: "struct DataTypes.ProfileStruct", 475 | name: "", 476 | type: "tuple", 477 | }, 478 | ], 479 | stateMutability: "view", 480 | type: "function", 481 | }, 482 | { 483 | inputs: [{ internalType: "string", name: "handle", type: "string" }], 484 | name: "getProfileIdByHandle", 485 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 486 | stateMutability: "view", 487 | type: "function", 488 | }, 489 | { 490 | inputs: [ 491 | { internalType: "uint256", name: "profileId", type: "uint256" }, 492 | { internalType: "uint256", name: "pubId", type: "uint256" }, 493 | ], 494 | name: "getPub", 495 | outputs: [ 496 | { 497 | components: [ 498 | { 499 | internalType: "uint256", 500 | name: "profileIdPointed", 501 | type: "uint256", 502 | }, 503 | { internalType: "uint256", name: "pubIdPointed", type: "uint256" }, 504 | { internalType: "string", name: "contentURI", type: "string" }, 505 | { internalType: "address", name: "referenceModule", type: "address" }, 506 | { internalType: "address", name: "collectModule", type: "address" }, 507 | { internalType: "address", name: "collectNFT", type: "address" }, 508 | ], 509 | internalType: "struct DataTypes.PublicationStruct", 510 | name: "", 511 | type: "tuple", 512 | }, 513 | ], 514 | stateMutability: "view", 515 | type: "function", 516 | }, 517 | { 518 | inputs: [{ internalType: "uint256", name: "profileId", type: "uint256" }], 519 | name: "getPubCount", 520 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 521 | stateMutability: "view", 522 | type: "function", 523 | }, 524 | { 525 | inputs: [ 526 | { internalType: "uint256", name: "profileId", type: "uint256" }, 527 | { internalType: "uint256", name: "pubId", type: "uint256" }, 528 | ], 529 | name: "getPubPointer", 530 | outputs: [ 531 | { internalType: "uint256", name: "", type: "uint256" }, 532 | { internalType: "uint256", name: "", type: "uint256" }, 533 | ], 534 | stateMutability: "view", 535 | type: "function", 536 | }, 537 | { 538 | inputs: [ 539 | { internalType: "uint256", name: "profileId", type: "uint256" }, 540 | { internalType: "uint256", name: "pubId", type: "uint256" }, 541 | ], 542 | name: "getPubType", 543 | outputs: [ 544 | { internalType: "enum DataTypes.PubType", name: "", type: "uint8" }, 545 | ], 546 | stateMutability: "view", 547 | type: "function", 548 | }, 549 | { 550 | inputs: [ 551 | { internalType: "uint256", name: "profileId", type: "uint256" }, 552 | { internalType: "uint256", name: "pubId", type: "uint256" }, 553 | ], 554 | name: "getReferenceModule", 555 | outputs: [{ internalType: "address", name: "", type: "address" }], 556 | stateMutability: "view", 557 | type: "function", 558 | }, 559 | { 560 | inputs: [], 561 | name: "getState", 562 | outputs: [ 563 | { internalType: "enum DataTypes.ProtocolState", name: "", type: "uint8" }, 564 | ], 565 | stateMutability: "view", 566 | type: "function", 567 | }, 568 | { 569 | inputs: [ 570 | { internalType: "string", name: "name", type: "string" }, 571 | { internalType: "string", name: "symbol", type: "string" }, 572 | { internalType: "address", name: "newGovernance", type: "address" }, 573 | ], 574 | name: "initialize", 575 | outputs: [], 576 | stateMutability: "nonpayable", 577 | type: "function", 578 | }, 579 | { 580 | inputs: [ 581 | { internalType: "address", name: "owner", type: "address" }, 582 | { internalType: "address", name: "operator", type: "address" }, 583 | ], 584 | name: "isApprovedForAll", 585 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 586 | stateMutability: "view", 587 | type: "function", 588 | }, 589 | { 590 | inputs: [ 591 | { internalType: "address", name: "collectModule", type: "address" }, 592 | ], 593 | name: "isCollectModuleWhitelisted", 594 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 595 | stateMutability: "view", 596 | type: "function", 597 | }, 598 | { 599 | inputs: [ 600 | { internalType: "address", name: "followModule", type: "address" }, 601 | ], 602 | name: "isFollowModuleWhitelisted", 603 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 604 | stateMutability: "view", 605 | type: "function", 606 | }, 607 | { 608 | inputs: [ 609 | { internalType: "address", name: "profileCreator", type: "address" }, 610 | ], 611 | name: "isProfileCreatorWhitelisted", 612 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 613 | stateMutability: "view", 614 | type: "function", 615 | }, 616 | { 617 | inputs: [ 618 | { internalType: "address", name: "referenceModule", type: "address" }, 619 | ], 620 | name: "isReferenceModuleWhitelisted", 621 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 622 | stateMutability: "view", 623 | type: "function", 624 | }, 625 | { 626 | inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], 627 | name: "mintTimestampOf", 628 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 629 | stateMutability: "view", 630 | type: "function", 631 | }, 632 | { 633 | inputs: [ 634 | { 635 | components: [ 636 | { internalType: "uint256", name: "profileId", type: "uint256" }, 637 | { 638 | internalType: "uint256", 639 | name: "profileIdPointed", 640 | type: "uint256", 641 | }, 642 | { internalType: "uint256", name: "pubIdPointed", type: "uint256" }, 643 | { internalType: "bytes", name: "referenceModuleData", type: "bytes" }, 644 | { internalType: "address", name: "referenceModule", type: "address" }, 645 | { 646 | internalType: "bytes", 647 | name: "referenceModuleInitData", 648 | type: "bytes", 649 | }, 650 | ], 651 | internalType: "struct DataTypes.MirrorData", 652 | name: "vars", 653 | type: "tuple", 654 | }, 655 | ], 656 | name: "mirror", 657 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 658 | stateMutability: "nonpayable", 659 | type: "function", 660 | }, 661 | { 662 | inputs: [ 663 | { 664 | components: [ 665 | { internalType: "uint256", name: "profileId", type: "uint256" }, 666 | { 667 | internalType: "uint256", 668 | name: "profileIdPointed", 669 | type: "uint256", 670 | }, 671 | { internalType: "uint256", name: "pubIdPointed", type: "uint256" }, 672 | { internalType: "bytes", name: "referenceModuleData", type: "bytes" }, 673 | { internalType: "address", name: "referenceModule", type: "address" }, 674 | { 675 | internalType: "bytes", 676 | name: "referenceModuleInitData", 677 | type: "bytes", 678 | }, 679 | { 680 | components: [ 681 | { internalType: "uint8", name: "v", type: "uint8" }, 682 | { internalType: "bytes32", name: "r", type: "bytes32" }, 683 | { internalType: "bytes32", name: "s", type: "bytes32" }, 684 | { internalType: "uint256", name: "deadline", type: "uint256" }, 685 | ], 686 | internalType: "struct DataTypes.EIP712Signature", 687 | name: "sig", 688 | type: "tuple", 689 | }, 690 | ], 691 | internalType: "struct DataTypes.MirrorWithSigData", 692 | name: "vars", 693 | type: "tuple", 694 | }, 695 | ], 696 | name: "mirrorWithSig", 697 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 698 | stateMutability: "nonpayable", 699 | type: "function", 700 | }, 701 | { 702 | inputs: [], 703 | name: "name", 704 | outputs: [{ internalType: "string", name: "", type: "string" }], 705 | stateMutability: "view", 706 | type: "function", 707 | }, 708 | { 709 | inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], 710 | name: "ownerOf", 711 | outputs: [{ internalType: "address", name: "", type: "address" }], 712 | stateMutability: "view", 713 | type: "function", 714 | }, 715 | { 716 | inputs: [ 717 | { internalType: "address", name: "spender", type: "address" }, 718 | { internalType: "uint256", name: "tokenId", type: "uint256" }, 719 | { 720 | components: [ 721 | { internalType: "uint8", name: "v", type: "uint8" }, 722 | { internalType: "bytes32", name: "r", type: "bytes32" }, 723 | { internalType: "bytes32", name: "s", type: "bytes32" }, 724 | { internalType: "uint256", name: "deadline", type: "uint256" }, 725 | ], 726 | internalType: "struct DataTypes.EIP712Signature", 727 | name: "sig", 728 | type: "tuple", 729 | }, 730 | ], 731 | name: "permit", 732 | outputs: [], 733 | stateMutability: "nonpayable", 734 | type: "function", 735 | }, 736 | { 737 | inputs: [ 738 | { internalType: "address", name: "owner", type: "address" }, 739 | { internalType: "address", name: "operator", type: "address" }, 740 | { internalType: "bool", name: "approved", type: "bool" }, 741 | { 742 | components: [ 743 | { internalType: "uint8", name: "v", type: "uint8" }, 744 | { internalType: "bytes32", name: "r", type: "bytes32" }, 745 | { internalType: "bytes32", name: "s", type: "bytes32" }, 746 | { internalType: "uint256", name: "deadline", type: "uint256" }, 747 | ], 748 | internalType: "struct DataTypes.EIP712Signature", 749 | name: "sig", 750 | type: "tuple", 751 | }, 752 | ], 753 | name: "permitForAll", 754 | outputs: [], 755 | stateMutability: "nonpayable", 756 | type: "function", 757 | }, 758 | { 759 | inputs: [ 760 | { 761 | components: [ 762 | { internalType: "uint256", name: "profileId", type: "uint256" }, 763 | { internalType: "string", name: "contentURI", type: "string" }, 764 | { internalType: "address", name: "collectModule", type: "address" }, 765 | { 766 | internalType: "bytes", 767 | name: "collectModuleInitData", 768 | type: "bytes", 769 | }, 770 | { internalType: "address", name: "referenceModule", type: "address" }, 771 | { 772 | internalType: "bytes", 773 | name: "referenceModuleInitData", 774 | type: "bytes", 775 | }, 776 | ], 777 | internalType: "struct DataTypes.PostData", 778 | name: "vars", 779 | type: "tuple", 780 | }, 781 | ], 782 | name: "post", 783 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 784 | stateMutability: "nonpayable", 785 | type: "function", 786 | }, 787 | { 788 | inputs: [ 789 | { 790 | components: [ 791 | { internalType: "uint256", name: "profileId", type: "uint256" }, 792 | { internalType: "string", name: "contentURI", type: "string" }, 793 | { internalType: "address", name: "collectModule", type: "address" }, 794 | { 795 | internalType: "bytes", 796 | name: "collectModuleInitData", 797 | type: "bytes", 798 | }, 799 | { internalType: "address", name: "referenceModule", type: "address" }, 800 | { 801 | internalType: "bytes", 802 | name: "referenceModuleInitData", 803 | type: "bytes", 804 | }, 805 | { 806 | components: [ 807 | { internalType: "uint8", name: "v", type: "uint8" }, 808 | { internalType: "bytes32", name: "r", type: "bytes32" }, 809 | { internalType: "bytes32", name: "s", type: "bytes32" }, 810 | { internalType: "uint256", name: "deadline", type: "uint256" }, 811 | ], 812 | internalType: "struct DataTypes.EIP712Signature", 813 | name: "sig", 814 | type: "tuple", 815 | }, 816 | ], 817 | internalType: "struct DataTypes.PostWithSigData", 818 | name: "vars", 819 | type: "tuple", 820 | }, 821 | ], 822 | name: "postWithSig", 823 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 824 | stateMutability: "nonpayable", 825 | type: "function", 826 | }, 827 | { 828 | inputs: [ 829 | { internalType: "address", name: "from", type: "address" }, 830 | { internalType: "address", name: "to", type: "address" }, 831 | { internalType: "uint256", name: "tokenId", type: "uint256" }, 832 | ], 833 | name: "safeTransferFrom", 834 | outputs: [], 835 | stateMutability: "nonpayable", 836 | type: "function", 837 | }, 838 | { 839 | inputs: [ 840 | { internalType: "address", name: "from", type: "address" }, 841 | { internalType: "address", name: "to", type: "address" }, 842 | { internalType: "uint256", name: "tokenId", type: "uint256" }, 843 | { internalType: "bytes", name: "_data", type: "bytes" }, 844 | ], 845 | name: "safeTransferFrom", 846 | outputs: [], 847 | stateMutability: "nonpayable", 848 | type: "function", 849 | }, 850 | { 851 | inputs: [ 852 | { internalType: "address", name: "operator", type: "address" }, 853 | { internalType: "bool", name: "approved", type: "bool" }, 854 | ], 855 | name: "setApprovalForAll", 856 | outputs: [], 857 | stateMutability: "nonpayable", 858 | type: "function", 859 | }, 860 | { 861 | inputs: [{ internalType: "uint256", name: "profileId", type: "uint256" }], 862 | name: "setDefaultProfile", 863 | outputs: [], 864 | stateMutability: "nonpayable", 865 | type: "function", 866 | }, 867 | { 868 | inputs: [ 869 | { 870 | components: [ 871 | { internalType: "address", name: "wallet", type: "address" }, 872 | { internalType: "uint256", name: "profileId", type: "uint256" }, 873 | { 874 | components: [ 875 | { internalType: "uint8", name: "v", type: "uint8" }, 876 | { internalType: "bytes32", name: "r", type: "bytes32" }, 877 | { internalType: "bytes32", name: "s", type: "bytes32" }, 878 | { internalType: "uint256", name: "deadline", type: "uint256" }, 879 | ], 880 | internalType: "struct DataTypes.EIP712Signature", 881 | name: "sig", 882 | type: "tuple", 883 | }, 884 | ], 885 | internalType: "struct DataTypes.SetDefaultProfileWithSigData", 886 | name: "vars", 887 | type: "tuple", 888 | }, 889 | ], 890 | name: "setDefaultProfileWithSig", 891 | outputs: [], 892 | stateMutability: "nonpayable", 893 | type: "function", 894 | }, 895 | { 896 | inputs: [ 897 | { internalType: "uint256", name: "profileId", type: "uint256" }, 898 | { internalType: "address", name: "dispatcher", type: "address" }, 899 | ], 900 | name: "setDispatcher", 901 | outputs: [], 902 | stateMutability: "nonpayable", 903 | type: "function", 904 | }, 905 | { 906 | inputs: [ 907 | { 908 | components: [ 909 | { internalType: "uint256", name: "profileId", type: "uint256" }, 910 | { internalType: "address", name: "dispatcher", type: "address" }, 911 | { 912 | components: [ 913 | { internalType: "uint8", name: "v", type: "uint8" }, 914 | { internalType: "bytes32", name: "r", type: "bytes32" }, 915 | { internalType: "bytes32", name: "s", type: "bytes32" }, 916 | { internalType: "uint256", name: "deadline", type: "uint256" }, 917 | ], 918 | internalType: "struct DataTypes.EIP712Signature", 919 | name: "sig", 920 | type: "tuple", 921 | }, 922 | ], 923 | internalType: "struct DataTypes.SetDispatcherWithSigData", 924 | name: "vars", 925 | type: "tuple", 926 | }, 927 | ], 928 | name: "setDispatcherWithSig", 929 | outputs: [], 930 | stateMutability: "nonpayable", 931 | type: "function", 932 | }, 933 | { 934 | inputs: [ 935 | { internalType: "address", name: "newEmergencyAdmin", type: "address" }, 936 | ], 937 | name: "setEmergencyAdmin", 938 | outputs: [], 939 | stateMutability: "nonpayable", 940 | type: "function", 941 | }, 942 | { 943 | inputs: [ 944 | { internalType: "uint256", name: "profileId", type: "uint256" }, 945 | { internalType: "address", name: "followModule", type: "address" }, 946 | { internalType: "bytes", name: "followModuleInitData", type: "bytes" }, 947 | ], 948 | name: "setFollowModule", 949 | outputs: [], 950 | stateMutability: "nonpayable", 951 | type: "function", 952 | }, 953 | { 954 | inputs: [ 955 | { 956 | components: [ 957 | { internalType: "uint256", name: "profileId", type: "uint256" }, 958 | { internalType: "address", name: "followModule", type: "address" }, 959 | { 960 | internalType: "bytes", 961 | name: "followModuleInitData", 962 | type: "bytes", 963 | }, 964 | { 965 | components: [ 966 | { internalType: "uint8", name: "v", type: "uint8" }, 967 | { internalType: "bytes32", name: "r", type: "bytes32" }, 968 | { internalType: "bytes32", name: "s", type: "bytes32" }, 969 | { internalType: "uint256", name: "deadline", type: "uint256" }, 970 | ], 971 | internalType: "struct DataTypes.EIP712Signature", 972 | name: "sig", 973 | type: "tuple", 974 | }, 975 | ], 976 | internalType: "struct DataTypes.SetFollowModuleWithSigData", 977 | name: "vars", 978 | type: "tuple", 979 | }, 980 | ], 981 | name: "setFollowModuleWithSig", 982 | outputs: [], 983 | stateMutability: "nonpayable", 984 | type: "function", 985 | }, 986 | { 987 | inputs: [ 988 | { internalType: "uint256", name: "profileId", type: "uint256" }, 989 | { internalType: "string", name: "followNFTURI", type: "string" }, 990 | ], 991 | name: "setFollowNFTURI", 992 | outputs: [], 993 | stateMutability: "nonpayable", 994 | type: "function", 995 | }, 996 | { 997 | inputs: [ 998 | { 999 | components: [ 1000 | { internalType: "uint256", name: "profileId", type: "uint256" }, 1001 | { internalType: "string", name: "followNFTURI", type: "string" }, 1002 | { 1003 | components: [ 1004 | { internalType: "uint8", name: "v", type: "uint8" }, 1005 | { internalType: "bytes32", name: "r", type: "bytes32" }, 1006 | { internalType: "bytes32", name: "s", type: "bytes32" }, 1007 | { internalType: "uint256", name: "deadline", type: "uint256" }, 1008 | ], 1009 | internalType: "struct DataTypes.EIP712Signature", 1010 | name: "sig", 1011 | type: "tuple", 1012 | }, 1013 | ], 1014 | internalType: "struct DataTypes.SetFollowNFTURIWithSigData", 1015 | name: "vars", 1016 | type: "tuple", 1017 | }, 1018 | ], 1019 | name: "setFollowNFTURIWithSig", 1020 | outputs: [], 1021 | stateMutability: "nonpayable", 1022 | type: "function", 1023 | }, 1024 | { 1025 | inputs: [ 1026 | { internalType: "address", name: "newGovernance", type: "address" }, 1027 | ], 1028 | name: "setGovernance", 1029 | outputs: [], 1030 | stateMutability: "nonpayable", 1031 | type: "function", 1032 | }, 1033 | { 1034 | inputs: [ 1035 | { internalType: "uint256", name: "profileId", type: "uint256" }, 1036 | { internalType: "string", name: "imageURI", type: "string" }, 1037 | ], 1038 | name: "setProfileImageURI", 1039 | outputs: [], 1040 | stateMutability: "nonpayable", 1041 | type: "function", 1042 | }, 1043 | { 1044 | inputs: [ 1045 | { 1046 | components: [ 1047 | { internalType: "uint256", name: "profileId", type: "uint256" }, 1048 | { internalType: "string", name: "imageURI", type: "string" }, 1049 | { 1050 | components: [ 1051 | { internalType: "uint8", name: "v", type: "uint8" }, 1052 | { internalType: "bytes32", name: "r", type: "bytes32" }, 1053 | { internalType: "bytes32", name: "s", type: "bytes32" }, 1054 | { internalType: "uint256", name: "deadline", type: "uint256" }, 1055 | ], 1056 | internalType: "struct DataTypes.EIP712Signature", 1057 | name: "sig", 1058 | type: "tuple", 1059 | }, 1060 | ], 1061 | internalType: "struct DataTypes.SetProfileImageURIWithSigData", 1062 | name: "vars", 1063 | type: "tuple", 1064 | }, 1065 | ], 1066 | name: "setProfileImageURIWithSig", 1067 | outputs: [], 1068 | stateMutability: "nonpayable", 1069 | type: "function", 1070 | }, 1071 | { 1072 | inputs: [ 1073 | { 1074 | internalType: "enum DataTypes.ProtocolState", 1075 | name: "newState", 1076 | type: "uint8", 1077 | }, 1078 | ], 1079 | name: "setState", 1080 | outputs: [], 1081 | stateMutability: "nonpayable", 1082 | type: "function", 1083 | }, 1084 | { 1085 | inputs: [{ internalType: "address", name: "", type: "address" }], 1086 | name: "sigNonces", 1087 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 1088 | stateMutability: "view", 1089 | type: "function", 1090 | }, 1091 | { 1092 | inputs: [{ internalType: "bytes4", name: "interfaceId", type: "bytes4" }], 1093 | name: "supportsInterface", 1094 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 1095 | stateMutability: "view", 1096 | type: "function", 1097 | }, 1098 | { 1099 | inputs: [], 1100 | name: "symbol", 1101 | outputs: [{ internalType: "string", name: "", type: "string" }], 1102 | stateMutability: "view", 1103 | type: "function", 1104 | }, 1105 | { 1106 | inputs: [{ internalType: "uint256", name: "index", type: "uint256" }], 1107 | name: "tokenByIndex", 1108 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 1109 | stateMutability: "view", 1110 | type: "function", 1111 | }, 1112 | { 1113 | inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], 1114 | name: "tokenDataOf", 1115 | outputs: [ 1116 | { 1117 | components: [ 1118 | { internalType: "address", name: "owner", type: "address" }, 1119 | { internalType: "uint96", name: "mintTimestamp", type: "uint96" }, 1120 | ], 1121 | internalType: "struct IERC721Time.TokenData", 1122 | name: "", 1123 | type: "tuple", 1124 | }, 1125 | ], 1126 | stateMutability: "view", 1127 | type: "function", 1128 | }, 1129 | { 1130 | inputs: [ 1131 | { internalType: "address", name: "owner", type: "address" }, 1132 | { internalType: "uint256", name: "index", type: "uint256" }, 1133 | ], 1134 | name: "tokenOfOwnerByIndex", 1135 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 1136 | stateMutability: "view", 1137 | type: "function", 1138 | }, 1139 | { 1140 | inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], 1141 | name: "tokenURI", 1142 | outputs: [{ internalType: "string", name: "", type: "string" }], 1143 | stateMutability: "view", 1144 | type: "function", 1145 | }, 1146 | { 1147 | inputs: [], 1148 | name: "totalSupply", 1149 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 1150 | stateMutability: "view", 1151 | type: "function", 1152 | }, 1153 | { 1154 | inputs: [ 1155 | { internalType: "address", name: "from", type: "address" }, 1156 | { internalType: "address", name: "to", type: "address" }, 1157 | { internalType: "uint256", name: "tokenId", type: "uint256" }, 1158 | ], 1159 | name: "transferFrom", 1160 | outputs: [], 1161 | stateMutability: "nonpayable", 1162 | type: "function", 1163 | }, 1164 | { 1165 | inputs: [ 1166 | { internalType: "address", name: "collectModule", type: "address" }, 1167 | { internalType: "bool", name: "whitelist", type: "bool" }, 1168 | ], 1169 | name: "whitelistCollectModule", 1170 | outputs: [], 1171 | stateMutability: "nonpayable", 1172 | type: "function", 1173 | }, 1174 | { 1175 | inputs: [ 1176 | { internalType: "address", name: "followModule", type: "address" }, 1177 | { internalType: "bool", name: "whitelist", type: "bool" }, 1178 | ], 1179 | name: "whitelistFollowModule", 1180 | outputs: [], 1181 | stateMutability: "nonpayable", 1182 | type: "function", 1183 | }, 1184 | { 1185 | inputs: [ 1186 | { internalType: "address", name: "profileCreator", type: "address" }, 1187 | { internalType: "bool", name: "whitelist", type: "bool" }, 1188 | ], 1189 | name: "whitelistProfileCreator", 1190 | outputs: [], 1191 | stateMutability: "nonpayable", 1192 | type: "function", 1193 | }, 1194 | { 1195 | inputs: [ 1196 | { internalType: "address", name: "referenceModule", type: "address" }, 1197 | { internalType: "bool", name: "whitelist", type: "bool" }, 1198 | ], 1199 | name: "whitelistReferenceModule", 1200 | outputs: [], 1201 | stateMutability: "nonpayable", 1202 | type: "function", 1203 | }, 1204 | ]; 1205 | -------------------------------------------------------------------------------- /graphql/auth/generateChallenge.ts: -------------------------------------------------------------------------------- 1 | import { basicClient } from "../initClient"; 2 | 3 | const getChallengeQuery = ` 4 | query($request: ChallengeRequest!) { 5 | challenge(request: $request) { text } 6 | } 7 | `; 8 | 9 | /** 10 | * Generate a message the user can sign to sign in with Lens 11 | * https://docs.lens.xyz/docs/login#challenge 12 | */ 13 | export const generateChallenge = async (address: string) => { 14 | const response = await basicClient 15 | .query(getChallengeQuery, { 16 | request: { 17 | address, 18 | }, 19 | }) 20 | .toPromise(); 21 | 22 | return response.data.challenge.text; 23 | }; 24 | -------------------------------------------------------------------------------- /graphql/auth/getAccessToken.ts: -------------------------------------------------------------------------------- 1 | import { basicClient } from "../initClient"; 2 | 3 | const authenticateMutation = ` 4 | mutation($request: SignedAuthChallenge!) { 5 | authenticate(request: $request) { 6 | accessToken 7 | refreshToken 8 | } 9 | } 10 | `; 11 | 12 | /** 13 | * Use the signature from generateChallenge to get an access token 14 | * https://docs.lens.xyz/docs/login#authenticate 15 | */ 16 | export const authenticate = async (address: string, signature: string) => { 17 | const response = await basicClient 18 | .mutation(authenticateMutation, { 19 | request: { 20 | address, 21 | signature, 22 | }, 23 | }) 24 | .toPromise(); 25 | 26 | console.log(response); 27 | 28 | return response.data.authenticate; 29 | }; 30 | -------------------------------------------------------------------------------- /graphql/auth/refreshAccessToken.ts: -------------------------------------------------------------------------------- 1 | import parseJwt from "../../util/parseJwt"; 2 | import { basicClient, STORAGE_KEY } from "../initClient"; 3 | 4 | const refreshMutation = ` 5 | mutation Refresh( 6 | $refreshToken: Jwt! 7 | ) { 8 | refresh(request: { 9 | refreshToken: $refreshToken 10 | }) { 11 | accessToken 12 | refreshToken 13 | } 14 | } 15 | `; 16 | 17 | /** 18 | * An access token is sent to the API to authenticate the user. 19 | * The access token expires after 30 minutes. 20 | * The refresh token can be used to get a new access token. 21 | * This function loads the refresh token from local storage and uses it to get a new access token. 22 | */ 23 | export const refreshAccessToken = async () => { 24 | const localStorageValue = localStorage.getItem(STORAGE_KEY); 25 | if (!localStorageValue) return null; 26 | 27 | const response = await basicClient 28 | .mutation(refreshMutation, { 29 | refreshToken: JSON.parse(localStorageValue).refreshToken, 30 | }) 31 | .toPromise(); 32 | 33 | if (!response.data) return null; 34 | 35 | const { accessToken, refreshToken } = response.data.refresh; 36 | const exp = parseJwt(refreshToken).exp; 37 | 38 | localStorage.setItem( 39 | STORAGE_KEY, 40 | JSON.stringify({ 41 | accessToken, 42 | refreshToken, 43 | exp, 44 | }) 45 | ); 46 | 47 | return { 48 | accessToken, 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /graphql/initClient.ts: -------------------------------------------------------------------------------- 1 | import { createClient as createUrqlClient } from "urql"; 2 | import { refreshAccessToken } from "./auth/refreshAccessToken"; 3 | 4 | // The base graphql endpoint 5 | export const APIURL = "https://api.lens.dev"; 6 | 7 | // The key we use to store the access token + refresh token + expiry in local storage` 8 | export const STORAGE_KEY = "LH_STORAGE_KEY"; 9 | 10 | // The contract address of the Lens smart contract 11 | export const LENS_HUB_CONTRACT_ADDRESS = 12 | "0xDb46d1Dc155634FbC732f92E853b10B288AD5a1d"; 13 | 14 | // Export a basic unauthenticated client for read operations 15 | export const basicClient = createUrqlClient({ 16 | url: APIURL, 17 | }); 18 | 19 | // Create an authenticated client on behalf of the current user. 20 | export async function createClient() { 21 | // Read their access token from local storage 22 | const localStorageValue = localStorage.getItem(STORAGE_KEY); 23 | 24 | // If we can't find one, the user is not logged in. Return the basic client. 25 | if (!localStorageValue) { 26 | return basicClient; 27 | } 28 | 29 | // Same as above, but we parse the JSON 30 | const storageData = JSON.parse(localStorageValue); 31 | if (!storageData) { 32 | return basicClient; 33 | } 34 | 35 | // Get a fresh access token by using the refresh token that we just read from storage. 36 | const accessTokenReq = await refreshAccessToken(); 37 | if (!accessTokenReq) { 38 | return basicClient; 39 | } 40 | 41 | // Create a new authenticated client with the new access token as the auth header 42 | const urqlClient = createUrqlClient({ 43 | url: APIURL, 44 | fetchOptions: { 45 | headers: { 46 | "x-access-token": `Bearer ${accessTokenReq.accessToken}`, 47 | }, 48 | }, 49 | }); 50 | return urqlClient; 51 | } 52 | -------------------------------------------------------------------------------- /graphql/mutate/followUser.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "../initClient"; 2 | 3 | const followUserMutation = ` 4 | mutation($request: FollowRequest!) { 5 | createFollowTypedData(request: $request) { 6 | id 7 | expiresAt 8 | typedData { 9 | domain { 10 | name 11 | chainId 12 | version 13 | verifyingContract 14 | } 15 | types { 16 | FollowWithSig { 17 | name 18 | type 19 | } 20 | } 21 | value { 22 | nonce 23 | deadline 24 | profileIds 25 | datas 26 | } 27 | } 28 | } 29 | } 30 | `; 31 | 32 | /** 33 | * This uses the authenticated urql client (meaning we send the access token with the request) 34 | * It creates a follow signature that we can send to the Lens smart contract to call 35 | * the followWithSig function to follow a user. 36 | */ 37 | export const followUser = async (profileId: string) => { 38 | const authenticatedClient = await createClient(); 39 | 40 | const response = await authenticatedClient 41 | .mutation(followUserMutation, { 42 | request: { 43 | follow: [ 44 | { 45 | profile: profileId, 46 | }, 47 | ], 48 | }, 49 | }) 50 | .toPromise(); 51 | 52 | return response.data.createFollowTypedData; 53 | }; 54 | -------------------------------------------------------------------------------- /graphql/query/doesFollowUser.ts: -------------------------------------------------------------------------------- 1 | import { basicClient } from "../initClient"; 2 | 3 | const doesFollowQuery = ` 4 | query($request: DoesFollowRequest!) { 5 | doesFollow(request: $request) { 6 | followerAddress 7 | profileId 8 | follows 9 | } 10 | } 11 | `; 12 | 13 | /** 14 | * Load a user's profile by their handle. 15 | */ 16 | async function doesFollowUser( 17 | followerAddress: string, 18 | profileId: string 19 | ): Promise { 20 | const response = await basicClient 21 | .query(doesFollowQuery, { 22 | request: { 23 | followInfos: [ 24 | { 25 | followerAddress, 26 | profileId, 27 | }, 28 | ], 29 | }, 30 | }) 31 | .toPromise(); 32 | 33 | console.log("hello???", response.data.doesFollow); 34 | 35 | return response.data.doesFollow[0].follows; 36 | } 37 | 38 | export default doesFollowUser; 39 | -------------------------------------------------------------------------------- /graphql/query/getProfile.ts: -------------------------------------------------------------------------------- 1 | import Profile from "../../types/Profile"; 2 | import { basicClient } from "../initClient"; 3 | 4 | export const getProfileQuery = ` 5 | query Profile($handle: Handle!) { 6 | profile(request: { handle: $handle }) { 7 | id 8 | name 9 | bio 10 | picture { 11 | ... on MediaSet { 12 | original { 13 | url 14 | } 15 | } 16 | } 17 | handle 18 | } 19 | } 20 | `; 21 | 22 | /** 23 | * Load a user's profile by their handle. 24 | */ 25 | async function getProfile(handle: string): Promise { 26 | const response = await basicClient 27 | .query(getProfileQuery, { 28 | handle, 29 | }) 30 | .toPromise(); 31 | return response.data.profile as Profile; 32 | } 33 | 34 | export default getProfile; 35 | -------------------------------------------------------------------------------- /graphql/query/getProfileByAddress.ts: -------------------------------------------------------------------------------- 1 | import Profile from "../../types/Profile"; 2 | import { basicClient } from "../initClient"; 3 | 4 | export const getProfileQuery = ` 5 | query Profile($address: EthereumAddress!) { 6 | defaultProfile(request: { ethereumAddress: $address }) { 7 | id 8 | name 9 | bio 10 | picture { 11 | ... on MediaSet { 12 | original { 13 | url 14 | } 15 | } 16 | } 17 | handle 18 | } 19 | } 20 | `; 21 | 22 | /** 23 | * Get a Lens Profile using a wallet address 24 | * Returns null if the user does not have a profile 25 | */ 26 | async function getProfileByAddress(address: string): Promise { 27 | const response = await basicClient 28 | .query(getProfileQuery, { 29 | address, 30 | }) 31 | .toPromise(); 32 | 33 | return response.data.defaultProfile as Profile | null; 34 | } 35 | 36 | export default getProfileByAddress; 37 | -------------------------------------------------------------------------------- /graphql/query/getPublications.ts: -------------------------------------------------------------------------------- 1 | import { basicClient } from "../initClient"; 2 | 3 | export const getPublicationsQuery = ` 4 | query Publications($id: ProfileId!, $limit: LimitScalar) { 5 | publications(request: { 6 | profileId: $id, 7 | publicationTypes: [POST], 8 | limit: $limit 9 | }) { 10 | items { 11 | __typename 12 | ... on Post { 13 | ...PostFields 14 | } 15 | } 16 | } 17 | } 18 | fragment PostFields on Post { 19 | id 20 | metadata { 21 | ...MetadataOutputFields 22 | } 23 | onChainContentURI 24 | } 25 | fragment MetadataOutputFields on MetadataOutput { 26 | name, 27 | description, 28 | content, 29 | image, 30 | cover { 31 | original { 32 | url 33 | } 34 | }, 35 | tags, 36 | } 37 | `; 38 | 39 | /** 40 | * Load a user's publications by their profile id. 41 | */ 42 | async function getPublications(profileId: string, limit: number): Promise { 43 | const response = await basicClient 44 | .query(getPublicationsQuery, { 45 | id: profileId, 46 | limit: limit, 47 | }) 48 | .toPromise(); 49 | 50 | return response.data.publications.items as any[]; 51 | } 52 | 53 | export default getPublications; 54 | -------------------------------------------------------------------------------- /graphql/query/mostFollowedProfiles.ts: -------------------------------------------------------------------------------- 1 | import Profile from "../../types/Profile"; 2 | import { basicClient } from "../initClient"; 3 | 4 | const exploreProfiles = ` 5 | query ExploreProfiles { 6 | exploreProfiles(request: { sortCriteria: MOST_FOLLOWERS }) { 7 | items { 8 | id 9 | name 10 | bio 11 | handle 12 | picture { 13 | ... on MediaSet { 14 | original { 15 | url 16 | } 17 | } 18 | } 19 | stats { 20 | totalFollowers 21 | } 22 | } 23 | } 24 | } 25 | `; 26 | 27 | /** 28 | * Load the top 25 most followed profiles on Lens. 29 | */ 30 | async function mostFollowedProfiles(): Promise { 31 | const response = await basicClient.query(exploreProfiles, {}).toPromise(); 32 | return response.data.exploreProfiles.items as Profile[]; 33 | } 34 | 35 | export default mostFollowedProfiles; 36 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lens", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@tanstack/react-query": "^4.19.1", 13 | "@thirdweb-dev/react": "^3", 14 | "@thirdweb-dev/sdk": "^3", 15 | "ethers": "^5.7.2", 16 | "graphql": "^16.6.0", 17 | "next": "^13", 18 | "omit-deep": "^0.3.0", 19 | "react": "^18.2", 20 | "react-dom": "^18.2", 21 | "urql": "^3.0.3" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^18.11.12", 25 | "@types/omit-deep": "^0.3.0", 26 | "@types/react": "^18.0.26", 27 | "@types/react-dom": "^18.0.9", 28 | "eslint": "^8.29.0", 29 | "eslint-config-next": "^13", 30 | "typescript": "^4.9.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Hydrate, 3 | QueryClient, 4 | QueryClientProvider, 5 | } from "@tanstack/react-query"; 6 | import { ThirdwebProvider } from "@thirdweb-dev/react"; 7 | import type { AppProps } from "next/app"; 8 | import Header from "../components/Header/Header"; 9 | import "../styles/globals.css"; 10 | 11 | export default function App({ Component, pageProps }: AppProps) { 12 | // Initialize React Query Client 13 | const queryClient = new QueryClient(); 14 | 15 | // Specify what network you're going to interact with 16 | const activeChain = "mumbai"; 17 | 18 | return ( 19 | // For thirdweb functionality 20 | 21 | {/* For React Query functionality */} 22 | 23 | {/* For React Query supporting SSR */} 24 | 25 |
26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import mostFollowedProfiles from "../graphql/query/mostFollowedProfiles"; 3 | import styles from "../styles/Home.module.css"; 4 | import Image from "next/image"; 5 | import { MediaRenderer } from "@thirdweb-dev/react"; 6 | 7 | export default function Home() { 8 | // Load the top 25 most followed Lens profiles 9 | const { data, isLoading } = useQuery( 10 | ["mostFollowedProfiles"], 11 | mostFollowedProfiles 12 | ); 13 | 14 | return ( 15 | <> 16 |
17 |
18 | thirdweb 25 | sol 32 |
33 |

Lens Starter Kit

34 |

35 | Build a simple application using thirdweb and Lens! 36 |

37 | 38 |
39 | {isLoading ? ( 40 |

Loading...

41 | ) : ( 42 | data?.map((profile) => ( 43 | 48 | 57 |

{profile.name}

58 |

@{profile.handle}

59 |
60 | )) 61 | )} 62 |
63 |
64 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /pages/profile/[handle].tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { GetStaticProps, GetStaticPaths } from "next"; 3 | import { 4 | dehydrate, 5 | QueryClient, 6 | useMutation, 7 | useQuery, 8 | useQueryClient, 9 | } from "@tanstack/react-query"; 10 | import { 11 | MediaRenderer, 12 | useAddress, 13 | useContract, 14 | useSDK, 15 | useSigner, 16 | Web3Button, 17 | } from "@thirdweb-dev/react"; 18 | import getProfile from "../../graphql/query/getProfile"; 19 | import getPublications from "../../graphql/query/getPublications"; 20 | import PublicationCard from "../../components/Publication/PublicationCard"; 21 | import Publication from "../../types/Publication"; 22 | import useLensUser from "../../util/useLensUser"; 23 | import login from "../../util/login"; 24 | import { followUser } from "../../graphql/mutate/followUser"; 25 | import { LENS_HUB_CONTRACT_ADDRESS } from "../../graphql/initClient"; 26 | import { LENS_PROTOCOL_PROFILES_ABI } from "../../const/abis"; 27 | import { signedTypeData, splitSignature } from "../../util/ethers.service"; 28 | import styles from "../../styles/Profile.module.css"; 29 | import doesFollowUser from "../../graphql/query/doesFollowUser"; 30 | 31 | /** 32 | * Dynamic route to display a Lens profile and their publications given a handle 33 | */ 34 | export default function ProfilePage() { 35 | // Next.js Router: Load the user's handle from the URL 36 | const router = useRouter(); 37 | const { handle } = router.query; 38 | 39 | // Get the SDK and signer for us to use for interacting with the lens smart contract 40 | const sdk = useSDK(); 41 | const signer = useSigner(); 42 | 43 | // React Query 44 | const queryClient = useQueryClient(); 45 | 46 | // Get the currently connected wallet address 47 | const address = useAddress(); 48 | 49 | // See if we need to sign the user in before they try follow a user 50 | const { isSignedIn } = useLensUser(); 51 | 52 | // Load the same queries we did on the server-side. 53 | // Will load data instantly since it's already in the cache. 54 | const { data: profile, isLoading: loadingProfile } = useQuery( 55 | ["profile"], 56 | () => getProfile(handle as string) 57 | ); 58 | 59 | // When the profile is loaded, load the publications for that profile 60 | const { data: publications, isLoading: loadingPublications } = useQuery( 61 | ["publications"], 62 | () => getPublications(profile?.id as string, 10), 63 | { 64 | // Only run this query if the profile is loaded 65 | enabled: !!profile, 66 | } 67 | ); 68 | 69 | // Check to see if the connected wallet address follows this user 70 | const { data: doesFollow } = useQuery( 71 | ["follows", address, profile?.id], 72 | () => doesFollowUser(address as string, profile?.id as string), 73 | { 74 | // Only run this query if the profile is loaded 75 | enabled: !!profile && !!address, 76 | } 77 | ); 78 | 79 | // Connect to the Lens Hub smart contract using it's ABI and address 80 | const { contract: lensHubContract } = useContract( 81 | LENS_HUB_CONTRACT_ADDRESS, 82 | LENS_PROTOCOL_PROFILES_ABI 83 | ); 84 | 85 | const { mutateAsync: follow } = useMutation(() => followThisUser(), { 86 | // When the mutation is successful, invalidate the doesFollow query so it will re-run 87 | onSuccess: () => { 88 | queryClient.setQueryData(["follows", address, profile?.id], true); 89 | }, 90 | }); 91 | 92 | // Follow the user when the follow button is clicked 93 | // This function does the following: 94 | // 1. Runs the followUser GraphQL Mutation to generate a typedData object 95 | // 2. Signs the typedData object with the user's wallet 96 | // 3. Sends the signature to the smart contract to follow the user, 97 | // by calling the "followWithSig" function on the LensHub contract 98 | async function followThisUser() { 99 | if (!isSignedIn) { 100 | if (address && sdk) await login(address, sdk); 101 | } 102 | 103 | if (!profile || !signer) return; 104 | 105 | // 1. Runs the followUser GraphQL Mutation to generate a typedData object 106 | const result = await followUser(profile.id); 107 | const typedData = result.typedData; 108 | 109 | // 2. Signs the typedData object with the user's wallet 110 | const signature = await signedTypeData( 111 | signer, 112 | typedData.domain, 113 | typedData.types, 114 | typedData.value 115 | ); 116 | 117 | // 3. Sends the signature to the smart contract to follow the user, 118 | const { v, r, s } = splitSignature(signature); 119 | 120 | try { 121 | const tx = await lensHubContract?.call("followWithSig", { 122 | follower: address!, 123 | profileIds: typedData.value.profileIds, 124 | datas: typedData.value.datas, 125 | sig: { 126 | v, 127 | r, 128 | s, 129 | deadline: typedData.value.deadline, 130 | }, 131 | }); 132 | 133 | console.log("Followed user", tx); 134 | 135 | return tx; 136 | } catch (error) { 137 | console.error(error); 138 | } 139 | } 140 | 141 | if (loadingProfile) { 142 | return

Loading...

; 143 | } 144 | 145 | if (!profile) { 146 | return

Profile not found

; 147 | } 148 | 149 | return ( 150 |
151 |
152 | 161 |

{profile?.name}

162 |

@{profile?.handle}

163 | 164 | {doesFollow ? ( 165 | Following 166 | ) : ( 167 | follow()} 173 | className={styles.followButton} 174 | > 175 | Follow 176 | 177 | )} 178 | 179 |

{profile.bio}

180 |
181 | 182 | {loadingPublications ? ( 183 |

Loading publications...

184 | ) : ( 185 |
186 | {publications?.map((publication: Publication) => ( 187 | 188 | ))} 189 |
190 | )} 191 |
192 | ); 193 | } 194 | 195 | export const getStaticProps: GetStaticProps = async (context) => { 196 | // Load data for the profile page on the server-side 197 | const { handle } = context.params!; 198 | const queryClient = new QueryClient(); 199 | 200 | // "Pre-fetch" the data for the profile page. Meaning when 201 | // we use the useQuery it is already available in the cache 202 | await queryClient.prefetchQuery(["profile"], () => 203 | getProfile(handle as string) 204 | ); 205 | 206 | // Learn more here: https://tanstack.com/query/v4/docs/guides/ssr#using-hydration 207 | return { 208 | props: { 209 | dehydratedState: dehydrate(queryClient), 210 | }, 211 | }; 212 | }; 213 | 214 | export const getStaticPaths: GetStaticPaths = async () => { 215 | return { 216 | // Returning an empty array here means we 217 | // don't statically generate any paths at build time, which 218 | // is probably not the most optimal thing we could do. 219 | // You could change this behaviour to pre-render any profiles you want 220 | paths: [], 221 | fallback: "blocking", 222 | }; 223 | }; 224 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/lens/a3939bf0b3bf3a5a7283b0bc58b6a9b1f4bb1093/public/favicon.ico -------------------------------------------------------------------------------- /public/lens.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/lens/a3939bf0b3bf3a5a7283b0bc58b6a9b1f4bb1093/public/lens.jpeg -------------------------------------------------------------------------------- /public/thirdweb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-top: 96px; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | justify-content: center; 7 | width: 100vw; 8 | padding: 0 24px; 9 | } 10 | 11 | .iconContainer { 12 | display: flex; 13 | flex-direction: row; 14 | justify-content: center; 15 | align-items: center; 16 | gap: 16px; 17 | padding: 16px; 18 | } 19 | 20 | .h1 { 21 | margin-bottom: 0px; 22 | } 23 | 24 | .explain { 25 | font-size: 1.125rem; 26 | } 27 | 28 | .lightPurple { 29 | color: #e011a7; 30 | } 31 | 32 | .profileGrid { 33 | display: flex; 34 | justify-content: center; 35 | flex-direction: row; 36 | flex-wrap: wrap; 37 | } 38 | 39 | .profileContainer { 40 | border-radius: 16px; 41 | border: 1px solid grey; 42 | padding: 16px; 43 | margin: 16px; 44 | display: flex; 45 | flex-direction: column; 46 | align-items: center; 47 | width: 240px; 48 | overflow-x: auto; 49 | text-decoration: none; 50 | color: inherit; 51 | } 52 | 53 | /* on hover */ 54 | .profileContainer:hover { 55 | background-color: rgba(0, 0, 0, 0.25); 56 | color: white; 57 | 58 | /* animate */ 59 | transition: 0.3s; 60 | } 61 | 62 | .profileName { 63 | font-size: 1.25rem; 64 | padding-bottom: 0px; 65 | margin-bottom: 8px; 66 | } 67 | 68 | .profileHandle { 69 | margin-top: 0px; 70 | padding-top: 0px; 71 | padding: 8px; 72 | opacity: 0.85; 73 | } 74 | -------------------------------------------------------------------------------- /styles/Profile.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-top: 5em; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | width: 100vw; 7 | padding: 0 24px; 8 | } 9 | 10 | .profileOutlineContainer { 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | justify-content: center; 15 | width: 100%; 16 | max-width: 1000px; 17 | margin-bottom: 32px; 18 | } 19 | 20 | .profileName { 21 | font-size: 1.25rem; 22 | padding-bottom: 0px; 23 | margin-bottom: 2px; 24 | } 25 | 26 | .profileBio { 27 | max-width: 720px; 28 | } 29 | 30 | .profileHandle { 31 | margin-top: 0px; 32 | padding-top: 0px; 33 | opacity: 0.7; 34 | } 35 | 36 | .publicationsContainer { 37 | display: flex; 38 | flex-direction: column; 39 | align-items: center; 40 | justify-content: center; 41 | width: 100%; 42 | max-width: 1000px; 43 | gap: 1em; 44 | } 45 | 46 | .followButton { 47 | background-color: var(--tw-color1); 48 | border-radius: 8px; 49 | color: white; 50 | font-weight: 600; 51 | font-size: 1rem; 52 | padding: 8px 16px; 53 | border: none; 54 | } 55 | 56 | .following { 57 | opacity: 0.75; 58 | padding: 0px; 59 | margin: 0px; 60 | } 61 | 62 | .followButton:hover { 63 | background-color: var(--tw-color1-hover); 64 | cursor: pointer; 65 | transition: 0.6s; 66 | } 67 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"); 2 | 3 | /* Box sizing rules */ 4 | *, 5 | *::before, 6 | *::after { 7 | box-sizing: border-box; 8 | } 9 | 10 | /* Set core body defaults */ 11 | body { 12 | min-height: 100vh; 13 | text-rendering: optimizeSpeed; 14 | line-height: 1.5; 15 | padding-bottom: 250px; 16 | } 17 | 18 | /* Inherit fonts for inputs and buttons */ 19 | input, 20 | button, 21 | textarea, 22 | select { 23 | font: inherit; 24 | } 25 | 26 | :root { 27 | --background-color: #1c1e21; 28 | --white: #ffffff; 29 | --tw-color1: #f213a4; 30 | --tw-color2: #5204bf; 31 | --tw-color1-hover: #9333ea; 32 | } 33 | 34 | body { 35 | background: var(--background-color); 36 | font-family: "Inter", sans-serif; 37 | color: var(--white); 38 | display: flex; 39 | margin: 0; 40 | text-align: center; 41 | } 42 | 43 | h2 { 44 | font-size: 2rem; 45 | } 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /types/Profile.ts: -------------------------------------------------------------------------------- 1 | type Profile = { 2 | id: string; 3 | name: string; 4 | bio: string; 5 | handle: string; 6 | picture: { 7 | original: { 8 | url: string; 9 | }; 10 | }; 11 | stats: { 12 | totalFollowers: number; 13 | }; 14 | __typename: "Profile"; 15 | }; 16 | 17 | export default Profile; 18 | -------------------------------------------------------------------------------- /types/Publication.ts: -------------------------------------------------------------------------------- 1 | type Publication = { 2 | __typename: string; 3 | id: string; 4 | metadata: { 5 | name: string; 6 | description: string; 7 | content: string; 8 | image: string | null; 9 | cover: { 10 | original: { 11 | url: string; 12 | }; 13 | } | null; 14 | tags: []; 15 | }; 16 | onChainContentURI: string; 17 | }; 18 | 19 | export default Publication; 20 | -------------------------------------------------------------------------------- /util/ethers.service.ts: -------------------------------------------------------------------------------- 1 | // Modified from official Lens example: 2 | // https://github.com/lens-protocol/api-examples/blob/master/src/ethers.service.ts 3 | 4 | import { TypedDataDomain } from "@ethersproject/abstract-signer"; 5 | import { utils } from "ethers"; 6 | import { omit } from "./helpers"; 7 | 8 | export const signedTypeData = ( 9 | signer: any, 10 | domain: TypedDataDomain, 11 | types: Record, 12 | value: Record 13 | ) => { 14 | // remove the __typedname from the signature! 15 | return signer._signTypedData( 16 | omit(domain, ["__typename"]), 17 | omit(types, ["__typename"]), 18 | omit(value, ["__typename"]) 19 | ); 20 | }; 21 | 22 | export const splitSignature = (signature: string) => { 23 | return utils.splitSignature(signature); 24 | }; 25 | -------------------------------------------------------------------------------- /util/helpers.ts: -------------------------------------------------------------------------------- 1 | // Modified from official Lens example: 2 | // https://github.com/lens-protocol/api-examples/blob/master/src/helpers.ts 3 | 4 | import omitDeep from "omit-deep"; 5 | 6 | export const sleep = (milliseconds: number): Promise => { 7 | return new Promise((resolve) => setTimeout(resolve, milliseconds)); 8 | }; 9 | 10 | export const omit = (object: any, name: string[]) => { 11 | return omitDeep(object, name); 12 | }; 13 | -------------------------------------------------------------------------------- /util/login.ts: -------------------------------------------------------------------------------- 1 | import { ThirdwebSDK } from "@thirdweb-dev/sdk"; 2 | import { generateChallenge } from "../graphql/auth/generateChallenge"; 3 | import { authenticate } from "../graphql/auth/getAccessToken"; 4 | import { STORAGE_KEY } from "../graphql/initClient"; 5 | import parseJwt from "./parseJwt"; 6 | 7 | /** 8 | * Function that signs the user into Lens by generating a challenge and signing it with their wallet. 9 | */ 10 | export default async function login(address: string, sdk: ThirdwebSDK) { 11 | if (!address || !sdk) return; 12 | 13 | try { 14 | // Generate Auth Challenge 15 | const challenge = await generateChallenge(address); 16 | 17 | // Sign the challenge message 18 | const signature = await sdk.wallet.sign(challenge); 19 | 20 | // Send the signature to the API to get an access token + refresh token 21 | const { accessToken, refreshToken } = await authenticate( 22 | address, 23 | signature 24 | ); 25 | 26 | // Now let's store the authentication information in local storage 27 | const accessTokenData = parseJwt(accessToken); 28 | localStorage.setItem( 29 | STORAGE_KEY, // This is the key we use to store the authentication information in local storage 30 | JSON.stringify({ 31 | accessToken, 32 | refreshToken, 33 | exp: accessTokenData.exp, 34 | }) 35 | ); 36 | 37 | return address; 38 | } catch (error) { 39 | console.error(error); 40 | alert("Error signing in"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /util/parseJwt.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility function for parsing JWT tokens such as those returned by the 3 | * Lens GraphQL API (access tokens and refresh tokens). 4 | */ 5 | export default function parseJwt(token: string) { 6 | var base64Url = token.split(".")[1]; 7 | var base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); 8 | var jsonPayload = decodeURIComponent( 9 | atob(base64) 10 | .split("") 11 | .map(function (c) { 12 | return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); 13 | }) 14 | .join("") 15 | ); 16 | 17 | return JSON.parse(jsonPayload); 18 | } 19 | -------------------------------------------------------------------------------- /util/useLensUser.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import { useAddress } from "@thirdweb-dev/react"; 3 | import { useEffect, useState } from "react"; 4 | import { STORAGE_KEY } from "../graphql/initClient"; 5 | import getProfileByAddress from "../graphql/query/getProfileByAddress"; 6 | 7 | /** 8 | * Hook to load the currently signed in Lens user's profile. 9 | * Returns: 10 | * - isSignedIn: If they are currently authenticated and their token hasn't expired 11 | * - loadingSignin: If we are currently checking if they are signed in 12 | * - profile: The profile of the currently signed in user 13 | * - loadingProfile: If we are currently loading the profile 14 | */ 15 | export default function useLensUser() { 16 | const address = useAddress(); 17 | const [isSignedIn, setIsSignedIn] = useState(false); 18 | const [loadingSignIn, setLoadingSignIn] = useState(false); 19 | 20 | useEffect(() => { 21 | if (typeof window === "undefined") return; 22 | 23 | // Read their access token from local storage 24 | const localStorageValue = localStorage.getItem(STORAGE_KEY); 25 | 26 | // Boolean flag to see if they have any authentication information stored in local storage 27 | const auth = localStorageValue ? JSON.parse(localStorageValue) : null; 28 | 29 | // If they do, check if their access token has expired 30 | if (auth) { 31 | // If it has, we say they're not signed in. 32 | const expired = auth.exp < Date.now() / 1000; 33 | setIsSignedIn(!expired); 34 | } else { 35 | setIsSignedIn(false); 36 | } 37 | 38 | setLoadingSignIn(false); 39 | }, [address]); 40 | 41 | // If they're signed in, we load their profile by querying the API 42 | const { data: profile, isLoading: loadingProfile } = useQuery( 43 | ["profile", address], 44 | () => getProfileByAddress(address as string), 45 | { 46 | enabled: !!address && isSignedIn, 47 | } 48 | ); 49 | 50 | return { 51 | isSignedIn, 52 | setIsSignedIn, 53 | loadingSignIn, 54 | profile, 55 | loadingProfile, 56 | }; 57 | } 58 | --------------------------------------------------------------------------------