From 5e9167b265be6ff56df773363bda138347bbb6da Mon Sep 17 00:00:00 2001
From: Burnt Nerve
Date: Thu, 19 Dec 2024 02:49:19 -0600
Subject: [PATCH] feat: add NFT minting functionality and NFT list component
---
.gitignore | 1 +
apps/demo-app/package.json | 3 +-
apps/demo-app/pages/api/mint-soulbound.ts | 63 ++++++++++++++++++++++
apps/demo-app/src/app/page.tsx | 64 ++++++++++++++++++++++-
apps/demo-app/src/components/NftList.tsx | 16 ++++++
package.json | 6 +++
pnpm-lock.yaml | 28 ++++++++--
scripts/instantiate-soulbound.mjs | 50 ++++++++++++++++++
8 files changed, 225 insertions(+), 6 deletions(-)
create mode 100644 apps/demo-app/pages/api/mint-soulbound.ts
create mode 100644 apps/demo-app/src/components/NftList.tsx
create mode 100644 scripts/instantiate-soulbound.mjs
diff --git a/.gitignore b/.gitignore
index 44a8d5d5..175e4513 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,6 +24,7 @@ yarn-debug.log*
yarn-error.log*
# local env files
+.env
.env.local
.env.development.local
.env.test.local
diff --git a/apps/demo-app/package.json b/apps/demo-app/package.json
index 72472d01..e214dd8e 100644
--- a/apps/demo-app/package.json
+++ b/apps/demo-app/package.json
@@ -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",
@@ -37,4 +36,4 @@
"tailwindcss": "^3.2.4",
"typescript": "^5.2.2"
}
-}
+}
\ No newline at end of file
diff --git a/apps/demo-app/pages/api/mint-soulbound.ts b/apps/demo-app/pages/api/mint-soulbound.ts
new file mode 100644
index 00000000..8a4ffc14
--- /dev/null
+++ b/apps/demo-app/pages/api/mint-soulbound.ts
@@ -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" });
+ }
+}
diff --git a/apps/demo-app/src/app/page.tsx b/apps/demo-app/src/app/page.tsx
index a41a02d2..a1e1696c 100644
--- a/apps/demo-app/src/app/page.tsx
+++ b/apps/demo-app/src/app/page.tsx
@@ -1,6 +1,6 @@
"use client";
import Link from "next/link";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import {
Abstraxion,
useAbstraxionAccount,
@@ -11,16 +11,21 @@ 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]: [
@@ -28,11 +33,18 @@ export default function Page(): JSX.Element {
React.Dispatch>,
] = useModal();
const [loading, setLoading] = useState(false);
+ const [isMinting, setIsMinting] = useState(false);
const [executeResult, setExecuteResult] =
useState(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);
@@ -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 {
+ 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 {
setLoading(true);
const msg = {
@@ -129,6 +183,14 @@ export default function Page(): JSX.Element {
) : null}
{signArb ? : null}
+
+
>
) : null}
No NFTs found.
;
+ }
+
+ return (
+ <>
+ NFT Token IDs:
+
+ {nfts.map((tokenId) => (
+ - {tokenId}
+ ))}
+
+ >
+ );
+}
diff --git a/package.json b/package.json
index badc74fb..7c9072c4 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index eaffcf6d..9f92bd0f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -21,6 +21,24 @@ importers:
'@changesets/cli':
specifier: ^2.27.1
version: 2.27.9
+ '@cosmjs/cosmwasm-stargate':
+ specifier: ^0.32.4
+ version: 0.32.4
+ '@cosmjs/encoding':
+ specifier: ^0.32.4
+ version: 0.32.4
+ '@cosmjs/proto-signing':
+ specifier: ^0.32.4
+ version: 0.32.4
+ '@cosmjs/stargate':
+ specifier: ^0.32.4
+ version: 0.32.4
+ '@cosmjs/tendermint-rpc':
+ specifier: ^0.32.4
+ version: 0.32.4
+ dotenv:
+ specifier: ^16.4.7
+ version: 16.4.7
eslint:
specifier: ^8.48.0
version: 8.57.0
@@ -54,9 +72,6 @@ importers:
'@cosmjs/amino':
specifier: ^0.32.4
version: 0.32.4
- '@cosmjs/cosmwasm-stargate':
- specifier: ^0.32.4
- version: 0.32.4
'@heroicons/react':
specifier: ^2.1.4
version: 2.1.5(react@18.2.0)
@@ -5358,6 +5373,11 @@ packages:
engines: {node: '>=12'}
dev: true
+ /dotenv@16.4.7:
+ resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
+ engines: {node: '>=12'}
+ dev: false
+
/dotenv@8.6.0:
resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==}
engines: {node: '>=10'}
@@ -6570,6 +6590,7 @@ packages:
/glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+ deprecated: Glob versions prior to v9 are no longer supported
dependencies:
fs.realpath: 1.0.0
inflight: 1.0.6
@@ -9041,6 +9062,7 @@ packages:
/rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
+ deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
dependencies:
glob: 7.2.3
diff --git a/scripts/instantiate-soulbound.mjs b/scripts/instantiate-soulbound.mjs
new file mode 100644
index 00000000..7551f422
--- /dev/null
+++ b/scripts/instantiate-soulbound.mjs
@@ -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);