├── .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 |
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 |
65 | {isWrongNetwork ? "Switch Network" : "Sign In with Lens"}
66 |
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 |
25 |
32 |
33 |
Lens Starter Kit
34 |
35 | Build a simple application using thirdweb and Lens!
36 |
37 |
38 |
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 |
--------------------------------------------------------------------------------