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);