Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
defimaxyy committed Jan 31, 2024
0 parents commit 03dcd10
Show file tree
Hide file tree
Showing 19 changed files with 1,171 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
NEYNAR_API_KEY=
THIRDWEB_ENGINE_URL=
THIRDWEB_ENGINE_WALLET=
THIRDWEB_ACCESS_TOKEN=
NEXT_PUBLIC_SMART_CONTRACT=
NEXT_PUBLIC_VERCEL_URL=
NEXT_PUBLIC_CHAIN_ID=
NODE_NEV=
NEXT_PUBLIC_WARPCAST_CAST_HASH=
39 changes: 39 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# Sentry Config File
.sentryclirc
16 changes: 16 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
async redirects() {
return [
{
source: "/",
destination: "/mint",
permanent: true,
},
];
},
reactStrictMode: true,
};

module.exports = nextConfig;
26 changes: 26 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "thirdweb-engine-farcaster-frame",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"bignumber.js": "^9.1.2",
"ethers": "^5.4.6",
"next": "14.1.0",
"nextjs-cors": "^2.2.0",
"react": "^18",
"react-dom": "^18",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"typescript": "^5"
}
}
Binary file added public/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions public/next.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/vercel.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 51 additions & 0 deletions src/api/fetchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { config } from "@/config/config";
import { z } from "zod";

const balanceResponseSchema = z.object({
result: z.object({
value: z.string(),
}),
});

const mintResponseSchema = z.object({
result: z.object({
queueId: z.string(),
}),
});

export const httpFetchBalanceStatus = async () => {
const response = await fetch(
`${config.thirdweb.engine.url}/backend-wallet/${config.thirdweb.chainId}/${config.thirdweb.engine.wallet}/get-balance`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${config.thirdweb.engine.accessToken}`,
"x-backend-wallet-address": config.thirdweb.engine.wallet!,
},
}
);

const result = await response.json();

return balanceResponseSchema.parse(result);
};

export const httpMint = async (receiver: string) => {
const response = await fetch(
`${config.thirdweb.engine.url}/contract/${config.thirdweb.chainId}/${config.contractAddress}/erc721/claim-to`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${config.thirdweb.engine.accessToken}`,
"x-backend-wallet-address": config.thirdweb.engine.wallet!,
},
body: JSON.stringify({ receiver: receiver.toLowerCase(), quantity: "1" }),
}
);

const result = await response.json();

return mintResponseSchema.parse(result);
};
17 changes: 17 additions & 0 deletions src/classes/ThirdWebEngine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ethers } from "ethers";
import { BigNumber } from "bignumber.js";
import { httpFetchBalanceStatus, httpMint } from "@/api/fetchers";

export class ThirdWebEngine {
public static isBalanceLow = async () => {
const { result } = await httpFetchBalanceStatus();
const formattedEther = ethers.utils.formatEther(result.value);
const totalFormattedBalance = BigNumber(formattedEther).dp(4).toNumber();
return totalFormattedBalance < 0.01;
};

public static mint = async (receiver: string) => {
const response = await httpMint(receiver);
return response;
};
}
75 changes: 75 additions & 0 deletions src/classes/Warpcast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { config } from "@/config/config";
import { z } from "zod";

// Neynar's api
const apiUrl = "https://api.neynar.com";

const recastsSchema = z.array(
z.object({
fid: z.number(),
fname: z.string().min(1),
})
);

const validateMessageSchema = z.object({
valid: z.literal(true),
action: z.object({
interactor: z.object({
fid: z.number(),
username: z.string(),
custody_address: z.string().startsWith("0x"),
}),
tapped_button: z.object({
index: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]),
}),
}),
});

export class Warpcast {
private static get computeDefaultHeader() {
return {
api_key: config.neynar.apiKey as string,
"content-type": "application/json",
};
}

private static async fetchAllRecasts() {
const url = `${apiUrl}/v2/farcaster/cast?identifier=${config.warpcast.castHas}&type=hash`;

const response = await fetch(url, {
headers: Warpcast.computeDefaultHeader,
method: "GET",
});

const data = await response
.json()
.then((res) => res.cast.reactions.recasts)
.then(recastsSchema.parse);

return data;
}

public static async hasRecasted(fid: number) {
const recasts = await Warpcast.fetchAllRecasts();
return recasts.some((recast) => recast.fid === fid);
}

public static async validateMessage(messageBytes: string) {
const url = `${apiUrl}/v2/farcaster/frame/validate`;

const response = await fetch(url, {
headers: Warpcast.computeDefaultHeader,
method: "POST",
body: JSON.stringify({
message_bytes_in_hex: messageBytes,
}),
});

const data = await response
.json()
.then((res) => res)
.then(validateMessageSchema.parse);

return data.action;
}
}
17 changes: 17 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const config = {
neynar: {
apiKey: process.env.NEYNAR_API_KEY,
},
contractAddress: process.env.NEXT_PUBLIC_SMART_CONTRACT,
warpcast: {
castHas: process.env.NEXT_PUBLIC_WARPCAST_CAST_HASH,
},
thirdweb: {
chainId: Number(process.env.NEXT_PUBLIC_CHAIN_ID),
engine: {
url: process.env.THIRDWEB_ENGINE_URL,
wallet: process.env.THIRDWEB_ENGINE_WALLET,
accessToken: process.env.THIRDWEB_ACCESS_TOKEN,
},
},
};
6 changes: 6 additions & 0 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import "@/styles/globals.css";
import type { AppProps } from "next/app";

export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
20 changes: 20 additions & 0 deletions src/pages/_document.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
return (
<Html lang="en">
<Head>
<meta property="og:title" content={`Thirdweb Frame`} />
<meta property="og:image" content={`<next_js_image_path>`} />
<meta property="fc:frame" content="vNext" />
<meta property="fc:frame:image" content={`<next_js_image_path>`} />
<meta property="fc:frame:post_url" content={`<next_js_image_path>`} />
<meta property="fc:frame:button:1" content="Get started" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
102 changes: 102 additions & 0 deletions src/pages/api/mint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { computeHtml } from "@/utils/compute-html";
import { Warpcast } from "../../classes/Warpcast";
import { NextApiRequest, NextApiResponse } from "next";
import NextCors from "nextjs-cors";
import { z } from "zod";
import { ThirdWebEngine } from "@/classes/ThirdWebEngine";

const requestBodyWarpcastSchema = z.object({
trustedData: z.object({
messageBytes: z.string().min(5),
}),
});

const requestQuerySchema = z.object({
type: z.union([z.literal("start"), z.literal("recast"), z.literal("mint")]),
});

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
await NextCors(req, res, {
methods: ["GET", "POST"],
origin: "*",
});

if (req.method !== "POST") {
return res.status(400).send({ error: "invalid method" });
}

try {
const { type } = requestQuerySchema.parse(req.query);

const { trustedData } = requestBodyWarpcastSchema.parse(req.body);

const action = await Warpcast.validateMessage(trustedData.messageBytes);

if (type === "start") {
const isBalanceLow = await ThirdWebEngine.isBalanceLow();

if (isBalanceLow) {
return res.status(200).send(
computeHtml({
imagePath: "<next_js_image_path>",
postType: "follow",
content: "Sorry we went out of gas :(",
})
);
}

return res.status(200).send(
computeHtml({
imagePath: "<next_js_image_path>",
postType: "recast",
content: "Re-cast to mint",
})
);
}

if (type === "recast") {
const hasRecasted = await Warpcast.hasRecasted(action.interactor.fid);

if (!hasRecasted) {
return res.status(200).send(
computeHtml({
imagePath: "<next_js_image_path>",
postType: "recast",
content: "Re-cast is required to mint the NFT",
})
);
}

return res.status(200).send(
computeHtml({
imagePath: "<next_js_image_path>",
postType: "mint",
content: "Mint NFT",
})
);
}

if (type === "mint") {
await ThirdWebEngine.mint(action.interactor.custody_address);

return res.status(200).send(
computeHtml({
imagePath: "<next_js_image_path>",
postType: "start", // Do your own custom post_url after user has minted the NFT + clicks your button
content: "Congrats! You minted the NFT",
})
);
}
} catch (err) {
return res.status(200).send(
computeHtml({
imagePath: "<next_js_image_path>",
postType: "start",
content: "Something went wrong",
})
);
}
}
3 changes: 3 additions & 0 deletions src/pages/mint.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function FramesMint() {
return <h1>{`Thirdweb Engine Farcaster Frame :)`}</h1>;
}
Loading

0 comments on commit 03dcd10

Please sign in to comment.