Skip to content

Commit

Permalink
feat: add NFT minting functionality and NFT list component
Browse files Browse the repository at this point in the history
  • Loading branch information
BurntNerve committed Dec 19, 2024
1 parent 683a738 commit 5e9167b
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 6 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ yarn-debug.log*
yarn-error.log*

# local env files
.env
.env.local
.env.development.local
.env.test.local
Expand Down
3 changes: 1 addition & 2 deletions apps/demo-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"@burnt-labs/signers": "workspace:*",
"@burnt-labs/ui": "workspace:*",
"@cosmjs/amino": "^0.32.4",
"@cosmjs/cosmwasm-stargate": "^0.32.4",
"@heroicons/react": "^2.1.4",
"@keplr-wallet/cosmos": "^0.12.80",
"cosmjs-types": "^0.9.0",
Expand All @@ -37,4 +36,4 @@
"tailwindcss": "^3.2.4",
"typescript": "^5.2.2"
}
}
}
63 changes: 63 additions & 0 deletions apps/demo-app/pages/api/mint-soulbound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { DirectSecp256k1Wallet } from "@cosmjs/proto-signing";
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { Tendermint37Client } from "@cosmjs/tendermint-rpc";
import { GasPrice } from "@cosmjs/stargate";
import { fromHex } from "@cosmjs/encoding";
import { createHash } from "crypto";

const rpcEndpoint = "https://rpc.xion-testnet-1.burnt.com:443";
const key = process.env.PRIVATE_KEY;
const contractAddress =
"xion1rcdjfs8f0dqrfyep28m6rgfuw5ue2y788lk5jsj0ll5f2jekh6yqm95y32";

export default async function handler(req, res) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}

const { address } = req.body;
if (!address) {
return res.status(400).json({ error: "Address is required" });
} else if (!key) {
return res.status(400).json({ error: "Private key is required" });
}

try {
const tendermint = await Tendermint37Client.connect(rpcEndpoint);
const signer = await DirectSecp256k1Wallet.fromKey(fromHex(key), "xion");
const [accountData] = await signer.getAccounts();

const client = await SigningCosmWasmClient.createWithSigner(
tendermint,
signer,
{
gasPrice: GasPrice.fromString("0.001uxion"),
},
);

const uniqueString = `${Date.now()}-${Math.random()}`;
const tokenId = createHash("sha256").update(uniqueString).digest("hex");

const msg = {
mint: {
token_id: tokenId,
owner: address,
token_uri: null,
extension: null,
},
};

const fee = "auto";

const result = await client.execute(
accountData.address,
contractAddress,
msg,
fee,
);
return res.status(200).json({ txHash: result.transactionHash, tokenId });
} catch (error) {
console.error("Error executing transaction:", error);
return res.status(500).json({ error: "Failed to execute transaction" });
}
}
64 changes: 63 additions & 1 deletion apps/demo-app/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import { useEffect, useState } from "react";
import {
Abstraxion,
useAbstraxionAccount,
Expand All @@ -11,28 +11,40 @@ import { Button } from "@burnt-labs/ui";
import "@burnt-labs/ui/dist/index.css";
import type { ExecuteResult } from "@cosmjs/cosmwasm-stargate";
import { SignArb } from "../components/sign-arb.tsx";
import NftList from "../components/NftList.tsx";

const seatContractAddress =
"xion1z70cvc08qv5764zeg3dykcyymj5z6nu4sqr7x8vl4zjef2gyp69s9mmdka";

const soulboundNftContractAddress =
"xion1rcdjfs8f0dqrfyep28m6rgfuw5ue2y788lk5jsj0ll5f2jekh6yqm95y32";

type ExecuteResultOrUndefined = ExecuteResult | undefined;

export default function Page(): JSX.Element {
// Abstraxion hooks
const { data: account } = useAbstraxionAccount();
const { client, signArb, logout } = useAbstraxionSigningClient();
const [nfts, setNfts] = useState([]);

// General state hooks
const [, setShowModal]: [
boolean,
React.Dispatch<React.SetStateAction<boolean>>,
] = useModal();
const [loading, setLoading] = useState(false);
const [isMinting, setIsMinting] = useState(false);
const [executeResult, setExecuteResult] =
useState<ExecuteResultOrUndefined>(undefined);

const blockExplorerUrl = `https://explorer.burnt.com/xion-testnet-1/tx/${executeResult?.transactionHash}`;

useEffect(() => {
if (account.bech32Address) {
void fetchNfts(account.bech32Address);
}
}, [account.bech32Address, client]);

function getTimestampInSeconds(date: Date | null): number {
if (!date) return 0;
const d = new Date(date);
Expand All @@ -44,6 +56,48 @@ export default function Page(): JSX.Element {
const oneYearFromNow = new Date();
oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1);

async function fetchNfts(owner: string): Promise<void> {
if (!client || !owner) return;
setLoading(true);
try {
const result = await client.queryContractSmart(
soulboundNftContractAddress,
{
tokens: { owner, start_after: null, limit: 100 },
},
);
setNfts(result.tokens || []);
} catch (error) {
console.error("Error fetching NFTs:", error);
} finally {
setLoading(false);
}
}

async function handleMint() {
if (!account.bech32Address) return;
setIsMinting(true);
setLoading(true);
try {
const response = await fetch("/api/mint-soulbound", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ address: account.bech32Address }),
});
const data = await response.json();

if (data.txHash) {
await new Promise((resolve) => setTimeout(resolve, 5000));
await fetchNfts(account.bech32Address);
}
} catch (error) {
console.error("Error minting NFT:", error);
} finally {
setIsMinting(false);
setLoading(false);
}
}

async function claimSeat(): Promise<void> {
setLoading(true);
const msg = {
Expand Down Expand Up @@ -129,6 +183,14 @@ export default function Page(): JSX.Element {
</Button>
) : null}
{signArb ? <SignArb /> : null}
<NftList nfts={nfts} />
<Button
onClick={handleMint}
fullWidth
disabled={isMinting || loading}
>
{loading ? "MINTING..." : "MINT NFT"}
</Button>
</>
) : null}
<Abstraxion
Expand Down
16 changes: 16 additions & 0 deletions apps/demo-app/src/components/NftList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export default function NftList({ nfts }) {
if (!nfts || nfts.length === 0) {
return <p>No NFTs found.</p>;
}

return (
<>
<h2>NFT Token IDs:</h2>
<ul>
{nfts.map((tokenId) => (
<li key={tokenId}>{tokenId}</li>
))}
</ul>
</>
);
}
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
"@burnt-labs/tsconfig": "workspace:*",
"@changesets/changelog-github": "^0.5.0",
"@changesets/cli": "^2.27.1",
"@cosmjs/cosmwasm-stargate": "^0.32.4",
"@cosmjs/encoding": "^0.32.4",
"@cosmjs/proto-signing": "^0.32.4",
"@cosmjs/stargate": "^0.32.4",
"@cosmjs/tendermint-rpc": "^0.32.4",
"dotenv": "^16.4.7",
"eslint": "^8.48.0",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.3",
Expand Down
28 changes: 25 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions scripts/instantiate-soulbound.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { GasPrice } from "@cosmjs/stargate";
import { DirectSecp256k1Wallet } from "@cosmjs/proto-signing";
import { Tendermint37Client } from "@cosmjs/tendermint-rpc";
import { fromHex } from "@cosmjs/encoding";

import dotenv from "dotenv";

dotenv.config();

const key = process.env.PRIVATE_KEY;
if (!key) {
console.error("Private key is missing. Please check your .env file.");
process.exit(1);
}

const rpcEndpoint = "https://rpc.xion-testnet-1.burnt.com:443";
const contractCodeId = 1674; // Testnet Code ID

async function instantiateContract() {
const tendermint = await Tendermint37Client.connect(rpcEndpoint);
const signer = await DirectSecp256k1Wallet.fromKey(fromHex(key), "xion");

const [accountData] = await signer.getAccounts();
const client = await SigningCosmWasmClient.createWithSigner(
tendermint,
signer,
{
gasPrice: GasPrice.fromString("0.001uxion"),
},
);

const msg = {
name: "Soulbound Token",
symbol: "SBT",
minter: accountData.address,
};

const result = await client.instantiate(
accountData.address,
contractCodeId,
msg,
"Soulbound Token - Instantiation",
"auto",
{ admin: accountData.address },
);
console.log("Contract instantiated at address:", result.contractAddress);
}

instantiateContract().catch(console.error);

0 comments on commit 5e9167b

Please sign in to comment.