gingernft.gno

// Package gingernft is an open-mint GRC721 collection on gno.land.
// Anyone can mint an NFT to their own wallet. Each token's metadata is
// stored as its TokenURI following the standard GRC721/OpenSea pattern:
// a data:application/json;base64,... URI containing a metadata document
// {name, description, image, attributes}. The image itself is usually a
// data:image/...;base64 URI inside that JSON, which makes the whole NFT
// (art + metadata) live fully on-chain with no IPFS or server.
//
// Safety rules:
//   - Minting goes to the caller's own wallet only.
//   - Only a token's owner can change its URI, move it, or burn it.
//   - There is no admin, no owner withdrawal, and no way to take tokens
//     that aren't yours.
package gingernft

import (
	"errors"
	"strconv"
	"strings"

	"gno.land/p/nt/ufmt/v0"
)

var nft *BasicNFT

func init(cur realm) {
	nft = NewBasicNFT(0, cur, "LordG Ginger NFTs", "GNFT")
}

// Mint creates a new NFT owned by the caller.
// metadataURI must be a URI pointing at the token's metadata JSON —
// typically data:application/json;base64,... produced by the minting UI
// (max 300k chars). Plain https:// or ipfs:// URIs also work.
// Returns the new token ID.
func Mint(cur realm, metadataURI string) string {
	require(cur.Previous().IsUser(), "Mint from a user wallet only, no intermediary realms.")
	caller := cur.Previous().Address()
	require(metadataURI != "", "metadataURI is required.")
	require(len(metadataURI) <= 300_000, "metadataURI too long (max 300000 chars).")

	tid := TokenID(strconv.FormatInt(nft.TokenCount()+1, 10))
	checkErr(nft.Mint(caller, tid))
	_, err := nft.SetTokenURI(caller, tid, TokenURI(metadataURI))
	checkErr(err)
	return tid.String()
}

// --- write wrappers (GRC721 caller-validation pattern) ---

// TransferFrom moves a token; caller must be owner or approved.
func TransferFrom(cur realm, from, to address, tid string) error {
	require(cur.Previous().IsUser(), "user wallets only")
	return nft.TransferFrom(cur.Previous().Address(), from, to, TokenID(tid))
}

// Approve approves another address to move a token.
func Approve(cur realm, to address, tid string) error {
	require(cur.Previous().IsUser(), "user wallets only")
	return nft.Approve(cur.Previous().Address(), to, TokenID(tid))
}

// SetApprovalForAll approves or revokes an operator for all caller's tokens.
func SetApprovalForAll(cur realm, operator address, approved bool) error {
	require(cur.Previous().IsUser(), "user wallets only")
	return nft.SetApprovalForAll(cur.Previous().Address(), operator, approved)
}

// SetTokenURI updates a token's metadata URI. Owner only.
func SetTokenURI(cur realm, tid, metadataURI string) error {
	require(cur.Previous().IsUser(), "user wallets only")
	require(len(metadataURI) <= 300_000, "metadataURI too long (max 300000 chars).")
	_, err := nft.SetTokenURI(cur.Previous().Address(), TokenID(tid), TokenURI(metadataURI))
	return err
}

// Burn destroys a token the caller owns.
func Burn(cur realm, tid string) error {
	require(cur.Previous().IsUser(), "user wallets only")
	owner, err := nft.OwnerOf(TokenID(tid))
	if err != nil {
		return err
	}
	if owner != cur.Previous().Address() {
		return errors.New("only the token owner can burn it")
	}
	return nft.Burn(TokenID(tid))
}

// --- read-only views (queryable via gnokey/qeval and the UI) ---

func Name() string      { return nft.Name() }
func Symbol() string    { return nft.Symbol() }
func TokenCount() int64 { return nft.TokenCount() }

func BalanceOf(owner address) (int64, error) { return nft.BalanceOf(owner) }
func OwnerOf(tid string) (address, error)    { return nft.OwnerOf(TokenID(tid)) }
func GetTokenURI(tid string) (string, error)    { return nft.TokenURI(TokenID(tid)) }

// --- gnoweb page ---

// Render lists minted tokens. Metadata JSON lives in each TokenURI;
// this page shows IDs and owners, with a hint per token.
func Render(path string) string {
	if path != "" {
		return "404 — try the home page."
	}
	var sb strings.Builder
	sb.WriteString("# LordG Ginger NFTs\n\n")
	sb.WriteString(ufmt.Sprintf("Open-mint GRC721 collection · **%d minted**\n\n", nft.TokenCount()))
	sb.WriteString("Mint by calling `Mint(metadataURI)` — the NFT goes straight to your own wallet.\n")
	sb.WriteString("Metadata is stored in each token's URI as `data:application/json;base64,...`.\n\n")
	var i int64
	for i = 1; i <= nft.TokenCount(); i++ {
		tid := strconv.FormatInt(i, 10)
		owner, err := nft.OwnerOf(TokenID(tid))
		if err != nil {
			continue
		}
		sb.WriteString(ufmt.Sprintf("- **#%s** — owner `%s`\n", tid, owner.String()))
	}
	return sb.String()
}

func require(cond bool, msg string) {
	if !cond {
		panic(msg)
	}
}

func checkErr(err error) {
	if err != nil {
		panic(err)
	}
}