Skip to content

Privy MPC wallet demo #97

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions typescript/privy-MPC-wallet-contract-call/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

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

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
110 changes: 110 additions & 0 deletions typescript/privy-MPC-wallet-contract-call/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# πŸ“ BNB Ping Pong DApp β€” Privy MPC Wallet Demo

This is a minimal web3 DApp that demonstrates how to use [Privy Embedded Wallets](https://www.privy.io) to interact with a smart contract on the **BNB Testnet**. Players can click **Ping** or **Pong**, which sends an on-chain transaction via Privy, updating the contract state.

## πŸ“ Demo Details

- **LIVE WEBSITE:** [BNB PING PONG DEMO](https://mpc-bnb-demo.netlify.app/)
- **Network:** BNB Chain Testnet (Chain ID: `97`)
- **Verified Contract Code:** [`0x52943bFb088221cd6E3181fbc19081A6B34be948`](https://testnet.bscscan.com/address/0x52943bFb088221cd6E3181fbc19081A6B34be948)
- **RPC URL:** `https://data-seed-prebsc-1-s1.binance.org:8545/`

## πŸ“¦ Tech Stack

- Next.js 14 (App Router)
- Tailwind CSS
- Ethers.js
- Privy Embedded Wallet SDK
- React Hot Toast

## πŸ›  How to Run Locally

### 1. Clone the repo

```bash
git clone https://github.com/yourusername/bnb-pingpong.git
cd bnb-pingpong
```

### 2. Install dependencies

```bash
npm install
```

### 3. Configure environment variables

Copy the `.env.example` to `.env.local`:

```bash
cp .env.example .env.local
```

Then update the following fields with your **Privy credentials**:

```
NEXT_PUBLIC_PRIVY_APP_ID=YOUR_PRIVY_APP_ID
PRIVY_APP_SECRET=YOUR_PRIVY_APP_SECRET
```

### 4. Create a Privy Project

1. Go to [https://www.privy.io](https://www.privy.io)
2. Create a new account or log in
3. Create a new app
4. Enable:
- Embedded Wallet
- Email
- Social Media Logins
5. Copy your:
- **App ID** β†’ use for `NEXT_PUBLIC_PRIVY_APP_ID`
- **App Secret** β†’ use for `PRIVY_APP_SECRET`
6. Add `http://localhost:3000` to the allowed origins in your Privy dashboard

### 5. Start the dev server

```bash
npm run dev
```

Visit `http://localhost:3000` in your browser.

## πŸ“„ Smart Contract ABI

```solidity
function ping() external;
function pong() external;
function pingCount() view returns (uint256);
function pongCount() view returns (uint256);
function nextMove() view returns (string);
```

## πŸ”” UX Notes

- You will see a toast **after transaction is sent** (with link to BscScan)
- Toasts appear **above** Privy modals
- This demo uses Privy's `useSendTransaction()` with success callbacks

## πŸ§ͺ BNB Testnet Faucet

Get testnet BNB from:
πŸ‘‰ [https://bnb-faucet.netlify.app/](https://bnb-faucet.netlify.app/)

## πŸ“ .env.example

Use this template to set up your local `.env.local` file:

```env
NEXT_PUBLIC_PRIVY_APP_ID=YOUR_PRIVY_APP_ID
PRIVY_APP_SECRET=YOUR_PRIVY_APP_SECRET
PINGPONG_ADDRESS=0x52943bFb088221cd6E3181fbc19081A6B34be948
RPC_URL=https://data-seed-prebsc-1-s1.binance.org:8545/
NEXT_PUBLIC_PINGPONG_CONTRACT_ADDRESS=0x52943bFb088221cd6E3181fbc19081A6B34be948
NEXT_PUBLIC_RPC_URL=https://data-seed-prebsc-1-s1.binance.org:8545/
```

## πŸ“œ License

MIT β€” use freely for demos, hacks, and experimentation.

Made with πŸ’› on BNB Testnet.
47 changes: 47 additions & 0 deletions typescript/privy-MPC-wallet-contract-call/app/api/status/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { NextResponse } from "next/server";
import { ethers } from "ethers";

// Inline ABI with `as const` to preserve typings
const ABI = [
{
inputs: [],
name: "getGameStatus",
outputs: [
{ internalType: "uint256", name: "totalPings", type: "uint256" },
{ internalType: "uint256", name: "totalPongs", type: "uint256" },
{ internalType: "string", name: "nextMove", type: "string" },
],
stateMutability: "view",
type: "function",
},
] as const;

// Load env vars
const PINGPONG_ADDRESS = process.env.PINGPONG_ADDRESS!;
const RPC_URL = process.env.RPC_URL!;

export async function GET() {
try {
const provider = new ethers.JsonRpcProvider(RPC_URL);

// Contract with precise method typing
const contract = new ethers.Contract(
PINGPONG_ADDRESS,
ABI,
provider
) as ethers.Contract & {
getGameStatus: () => Promise<[bigint, bigint, string]>;
};

const [pings, pongs, next] = await contract.getGameStatus();

return NextResponse.json({
pingCount: Number(pings),
pongCount: Number(pongs),
nextMove: next,
});
} catch (error) {
console.error("Error fetching game status:", error);
return NextResponse.json({ error: "Failed to fetch game status" }, { status: 500 });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client";

import { PrivyProvider } from "@privy-io/react-auth";
import { bnbTestnet } from "../lib/bnbTestnet"; // adjust path as needed

export default function Providers({ children }: { children: React.ReactNode }) {
return (
<PrivyProvider
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!}
config={{
embeddedWallets: {
createOnLogin: "all-users",
},
defaultChain: bnbTestnet,
supportedChains: [bnbTestnet],
}}
>
{children}
</PrivyProvider>
);
}
160 changes: 160 additions & 0 deletions typescript/privy-MPC-wallet-contract-call/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
"use client";

import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { usePrivy, useSendTransaction } from "@privy-io/react-auth";
import { Interface } from "ethers";
import toast, { Toaster } from "react-hot-toast";

const abi = ["function ping() external", "function pong() external"];
const contractAddress = process.env.NEXT_PUBLIC_PINGPONG_CONTRACT_ADDRESS!;
const chainId = 97;
const explorerBase = "https://testnet.bscscan.com/tx/";

export default function DashboardPage() {
const router = useRouter();
const { ready, authenticated, user, logout } = usePrivy();
const iface = useMemo(() => new Interface(abi), []);
const { sendTransaction } = useSendTransaction();

const [game, setGame] = useState({
pingCount: 0,
pongCount: 0,
nextMove: "",
});

const [txHash, setTxHash] = useState<string | null>(null);

useEffect(() => {
if (ready && !authenticated) router.push("/");
else if (ready && authenticated) fetchStatus();
}, [ready, authenticated]);

useEffect(() => {
if (txHash) {
toast.success(
<span>
Tx Confirmed:{" "}
<a
href={`${explorerBase}${txHash}`}
target="_blank"
rel="noopener noreferrer"
className="underline text-yellow-300"
>
View on BscScan
</a>
</span>,
{
icon: "πŸ“",
}
);
setTxHash(null);
}
}, [txHash]);

const fetchStatus = async () => {
const res = await fetch("/api/status");
const data = await res.json();
setGame(data);
};

const handleMove = async (move: "ping" | "pong") => {
try {
const data = iface.encodeFunctionData(move, []);
const tx = await sendTransaction({
to: contractAddress,
data,
value: BigInt(0),
chainId,
});

if (tx.hash) {
console.log("Tx hash:", tx.hash);
setTxHash(tx.hash);
}

await fetchStatus();
} catch (err) {
console.error("Transaction Error:", err);
}
};

if (!ready) return null;

return (
<main className="min-h-screen bg-[#0d0d0d] text-white flex items-center justify-center px-4 relative z-[999999]">
<Toaster position="top-center" toastOptions={{ className: "!z-[999999]" }} />
{authenticated ? (
<div className="w-full max-w-lg">
{/* BNB Faucet link */}
<div className="text-center mb-6">
<p className="text-sm text-gray-400">
Need testnet BNB?{" "}
<a
href="https://bnb-faucet.netlify.app/"
target="_blank"
rel="noopener noreferrer"
className="underline text-yellow-400 hover:text-yellow-300 font-medium"
>
Get it from the BNB Faucet πŸ”—
</a>
</p>
</div>

{/* Interaction box */}
<div className="bg-[#1a1a1a] border border-yellow-400/30 rounded-2xl shadow-xl p-8 sm:p-10 text-center relative">
<div className="absolute top-4 right-4">
<button
onClick={logout}
className="bg-yellow-400 hover:bg-yellow-500 text-black text-xs px-4 py-2 rounded-md font-semibold shadow transition"
>
Logout
</button>
</div>

<h1 className="text-3xl sm:text-4xl font-extrabold text-yellow-400 mb-2 tracking-wide">
BNB Ping Pong πŸ“
</h1>
<p className="text-sm text-gray-400 mb-6">Let’s keep the rally going</p>

<div className="text-left mb-6">
<p className="text-xs text-gray-500 mb-1">Connected Wallet:</p>
<p className="text-yellow-300 font-mono text-sm break-all">
{user?.wallet?.address}
</p>
</div>

<div className="grid grid-cols-2 gap-6 mb-6">
<div className="bg-[#0d0d0d] border border-yellow-400/10 rounded-md p-4 shadow-inner">
<p className="text-2xl font-bold text-yellow-300">{game.pingCount}</p>
<p className="text-xs text-gray-400 mt-1">Total Pings</p>
</div>
<div className="bg-[#0d0d0d] border border-yellow-400/10 rounded-md p-4 shadow-inner">
<p className="text-2xl font-bold text-yellow-300">{game.pongCount}</p>
<p className="text-xs text-gray-400 mt-1">Total Pongs</p>
</div>
</div>

<p className="text-sm text-gray-400">Next move:</p>
<p className="text-xl font-semibold text-yellow-300 mb-6">{game.nextMove}</p>

<div className="flex justify-center gap-4">
<button
onClick={() => handleMove("ping")}
className="bg-yellow-400 hover:bg-yellow-500 text-black font-bold py-2 px-6 rounded-lg shadow transition hover:scale-105"
>
Ping
</button>
<button
onClick={() => handleMove("pong")}
className="bg-yellow-400 hover:bg-yellow-500 text-black font-bold py-2 px-6 rounded-lg shadow transition hover:scale-105"
>
Pong
</button>
</div>
</div>
</div>
) : null}
</main>
);
}
Binary file not shown.
Loading