Skip to content

Commit 8fa0122

Browse files
committed
Working end-to-end flow
1 parent 46a0820 commit 8fa0122

6 files changed

Lines changed: 205 additions & 138 deletions

File tree

.env.sample

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
1-
NEXT_PUBLIC_THIRDWEB_CLIENT_ID=6f4b9d28993ca599e4fc109a86ffae22
2-
NEXT_PUBLIC_WALLET_ADDRESS=0xE32Ac8F1eDB5DcF1F739D85278bA50501f5FAF47
1+
NEXT_PUBLIC_THIRDWEB_CLIENT_ID=
2+
NEXT_PUBLIC_WALLET_ADDRESS=
3+
NEXT_PUBLIC_NFT_CONTRACT_ADDRESS=
4+
PAY_WEBHOOK_SECRET=
5+
BACKEND_WALLET_ADDRESS=
6+
ENGINE_API_URL=
7+
ENGINE_ACCESS_TOKEN=

next.config.mjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/** @type {import('next').NextConfig} */
2-
const nextConfig = {};
2+
const nextConfig = {
3+
trailingSlash: true,
4+
};
35

46
export default nextConfig;

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12+
"@thirdweb-dev/engine": "^0.0.15",
13+
"lucide-react": "^0.439.0",
1214
"next": "14.2.8",
1315
"react": "^18",
1416
"react-dom": "^18",

src/app/api/route.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { NextResponse, NextRequest } from "next/server";
2+
import { Engine } from "@thirdweb-dev/engine";
3+
import crypto from "crypto";
4+
5+
const generateSignature = (
6+
body: string,
7+
timestamp: string,
8+
secret: string
9+
): string => {
10+
const payload = `${timestamp}.${body}`;
11+
return crypto.createHmac("sha256", secret).update(payload).digest("hex");
12+
};
13+
14+
const isValidSignature = (
15+
body: string,
16+
timestamp: string,
17+
signature: string,
18+
secret: string
19+
): boolean => {
20+
const expectedSignature = generateSignature(body, timestamp, secret);
21+
return crypto.timingSafeEqual(
22+
Buffer.from(expectedSignature),
23+
Buffer.from(signature)
24+
);
25+
};
26+
27+
const isExpired = (timestamp: string, expirationInSeconds: number): boolean => {
28+
const currentTime = Math.floor(Date.now() / 1000);
29+
return currentTime - parseInt(timestamp) > expirationInSeconds;
30+
};
31+
32+
export async function POST(req: NextRequest) {
33+
try {
34+
const signatureFromHeader = req.headers.get("X-Pay-Signature");
35+
const timestampFromHeader = req.headers.get("X-Pay-Timestamp");
36+
const body = await req.json();
37+
38+
if (!signatureFromHeader || !timestampFromHeader) {
39+
console.error("Missing signature or timestamp header");
40+
return NextResponse.json(
41+
{ error: "Missing signature or timestamp header" },
42+
{ status: 401 }
43+
);
44+
}
45+
46+
if (
47+
!isValidSignature(
48+
JSON.stringify(body),
49+
timestampFromHeader,
50+
signatureFromHeader,
51+
process.env.PAY_WEBHOOK_SECRET as string
52+
)
53+
) {
54+
console.error("Invalid Signature");
55+
return NextResponse.json({ error: "Invalid Signature" }, { status: 401 });
56+
}
57+
58+
if (isExpired(timestampFromHeader, 300)) {
59+
// Assuming expiration time is 5 minutes (300 seconds)
60+
console.error("Request has expired");
61+
return NextResponse.json(
62+
{ error: "Request has expired" },
63+
{ status: 401 }
64+
);
65+
}
66+
67+
const { data } = body;
68+
69+
if (
70+
data.buyWithFiatStatus &&
71+
data.buyWithFiatStatus.status === "ON_RAMP_TRANSFER_COMPLETED"
72+
) {
73+
const {
74+
fromAddress,
75+
purchaseData: { nftContractAddress, chainId, amount },
76+
} = body.data.buyWithFiatStatus;
77+
78+
// Validate the purchase and call the engine
79+
const result = await sendContractCallToEngine(
80+
fromAddress,
81+
nftContractAddress,
82+
chainId,
83+
amount
84+
);
85+
86+
return NextResponse.json({
87+
message: "Purchase processed successfully",
88+
result,
89+
});
90+
}
91+
92+
if (
93+
data.buyWithCryptoStatus &&
94+
data.buyWithCryptoStatus.status === "CRYPTO_SWAP_COMPLETED"
95+
) {
96+
const {
97+
fromAddress,
98+
purchaseData: { nftContractAddress, chainId, metadata },
99+
} = body.data.buyWithCryptoStatus;
100+
101+
// Validate the purchase and call the engine
102+
const result = await sendContractCallToEngine(
103+
fromAddress,
104+
nftContractAddress,
105+
chainId,
106+
metadata
107+
);
108+
109+
return NextResponse.json({
110+
message: "Purchase processed successfully",
111+
result,
112+
});
113+
}
114+
115+
return NextResponse.json({ message: "Status received" });
116+
} catch (error) {
117+
console.error("Error handling webhook:", error);
118+
return NextResponse.json(
119+
{ error: "Error processing webhook" },
120+
{ status: 500 }
121+
);
122+
}
123+
}
124+
125+
async function sendContractCallToEngine(
126+
fromAddress: string,
127+
nftContractAddress: string,
128+
chainId: string,
129+
metadata: Record<string, string> = {}
130+
) {
131+
try {
132+
const engine = new Engine({
133+
url: process.env.ENGINE_API_URL as string,
134+
accessToken: process.env.ENGINE_ACCESS_TOKEN as string,
135+
});
136+
137+
const response = await engine.erc721.mintTo(
138+
chainId,
139+
nftContractAddress,
140+
process.env.BACKEND_WALLET_ADDRESS as string,
141+
{
142+
receiver: fromAddress,
143+
metadata: metadata,
144+
}
145+
);
146+
147+
console.log("Response", response.result);
148+
149+
if (!response) {
150+
throw new Error("Error in Engine contract call");
151+
}
152+
153+
return response;
154+
} catch (error) {
155+
console.error("Error processing Engine contract call", error);
156+
return NextResponse.json(
157+
{ error: "Error processing Engine contract call" },
158+
{ status: 500 }
159+
);
160+
}
161+
}

src/app/components/DirectPayment.tsx

Lines changed: 0 additions & 125 deletions
This file was deleted.

src/app/page.tsx

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,46 @@
11
"use client";
22

33
import { createThirdwebClient } from "thirdweb";
4-
import { ConnectButton } from "thirdweb/react";
5-
import { useActiveAccount } from "thirdweb/react";
6-
import DirectPayment from "./components/DirectPayment";
4+
import { PayEmbed } from "thirdweb/react";
5+
import { baseSepolia } from "thirdweb/chains";
76

87
// create a thirdweb client
98
const client = createThirdwebClient({
109
clientId: `${process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID}`,
1110
});
1211

1312
export default function Home() {
14-
const account = useActiveAccount();
1513
return (
16-
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
17-
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
18-
<ConnectButton client={client} />
19-
{account && (
20-
<DirectPayment client={client} fromAddress={account.address} />
21-
)}
14+
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center justify-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
15+
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start justify-center">
16+
<PayEmbed
17+
client={client}
18+
theme={"dark"}
19+
payOptions={{
20+
mode: "direct_payment",
21+
buyWithFiat: { testMode: true },
22+
paymentInfo: {
23+
amount: "0.01",
24+
chain: baseSepolia,
25+
sellerAddress: process.env.NEXT_PUBLIC_WALLET_ADDRESS as string,
26+
},
27+
purchaseData: {
28+
nftContractAddress: process.env.NEXT_PUBLIC_NFT_CONTRACT_ADDRESS,
29+
chainId: baseSepolia.id,
30+
metadata: {
31+
name: "Pay Sample NFT",
32+
image:
33+
"ipfs://bafybeia3gb56ne2tuoujtwiorkruhaukzdou3u2o5fjpyf7mbbuuf6brtq/krabs.webp",
34+
amount: "0.01",
35+
},
36+
},
37+
metadata: {
38+
name: "Pay Sample NFT",
39+
image:
40+
"ipfs://bafybeia3gb56ne2tuoujtwiorkruhaukzdou3u2o5fjpyf7mbbuuf6brtq/krabs.webp",
41+
},
42+
}}
43+
/>
2244
</main>
2345
</div>
2446
);

0 commit comments

Comments
 (0)