Skip to content

Commit

Permalink
fix: GRC20 token payments and NFT token IDs
Browse files Browse the repository at this point in the history
  • Loading branch information
JJOptimist committed Feb 25, 2025
1 parent 7813091 commit 4c81209
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 27 deletions.
64 changes: 51 additions & 13 deletions examples/gno.land/r/jjoptimist/eventix/eventix.gno
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@ import (
"std"
"strconv"
"time"
"fmt"

"gno.land/p/demo/avl"
"gno.land/p/demo/grc/grc721"
"gno.land/p/demo/ufmt"
"gno.land/p/demo/avl/pager"
"gno.land/p/demo/grc/grc20"
)

type Event struct {
name string
description string
date time.Time
maxTickets int
price uint64 // price per ticket (in tokens)
paymentToken string // payment token symbol (e.g., ugnot or any GRC20 token)
price uint64
paymentToken interface{}
ticketsSold int
}

Expand All @@ -27,13 +29,33 @@ var (
tickets = grc721.NewBasicNFT("Event Ticket", "EVTIX")
)

func CreateEvent(name, description string, dateStr string, paymentToken string, maxTickets int, price uint64) uint64 {

func CreateEvent(name, description, dateStr string, paymentToken interface{}, maxTickets int, price uint64) uint64 {
// validate inputs
if maxTickets <= 0 {
panic("Maximum tickets must be greater than 0")
}

date, err := time.Parse("2006-01-02T15:04:05Z", dateStr)
if err != nil {
panic("Invalid date format. Use: YYYY-MM-DDThh:mm:ssZ")
}

// Validate payment token
switch pt := paymentToken.(type) {
case string:
// Native token (e.g., "ugnot")
if pt != "ugnot" {
panic("Unsupported native token")
}
case grc20.Teller:
// GRC20 token
if pt == nil {
panic("Invalid GRC20 token")
}
default:
panic("Unsupported payment token type")
}

newID := eventCounter + 1
event := Event{
name: name,
Expand All @@ -46,7 +68,7 @@ func CreateEvent(name, description string, dateStr string, paymentToken string,
}
events.Set(strconv.Itoa(int(newID)), event)
eventCounter = newID
std.Emit("EventCreated", ufmt.Sprintf("Event %d: %s created", newID, name))
std.Emit("EventCreated", "id", strconv.FormatUint(newID, 10), "name", name)
return newID
}

Expand All @@ -68,19 +90,35 @@ func BuyTicket(eventId uint64) {
panic("Event is sold out")
}

sent := std.GetOrigSend()
amount := sent.AmountOf(event.paymentToken)
if amount != int64(event.price) {
panic(ufmt.Sprintf("Please send exactly %d %s", event.price, event.paymentToken))
buyer := std.PrevRealm().Addr()

switch pt := event.paymentToken.(type) {
case string:
// Handle native token payment
if pt != "ugnot" {
panic("Unsupported native token")
}

sent := std.GetOrigSend()
if len(sent) != 1 || sent[0].Denom != "ugnot" || uint64(sent[0].Amount) != event.price {
panic("Invalid payment amount")
}

case grc20.Teller:
// Handle GRC20 payment
if err := pt.TransferFrom(buyer, std.CurrentRealm().Addr(), event.price); err != nil {
panic("GRC20 payment failed: " + err.Error())
}
default:
panic("Unsupported payment token type")
}

caller := std.PrevRealm().Addr()
tokenId := grc721.TokenID(strconv.Itoa(int(eventId)) + "-" + strconv.Itoa(event.ticketsSold+1))
tickets.Mint(caller, tokenId)
// Mint NFT ticket
tokenId := grc721.TokenID(fmt.Sprintf("event_%d_ticket_%d", eventId, event.ticketsSold+1))
tickets.Mint(buyer, tokenId)

event.ticketsSold++
events.Set(strconv.Itoa(int(eventId)), event)
std.Emit("TicketPurchased", ufmt.Sprintf("Ticket %s purchased for Event %d by %s", tokenId, eventId, caller))
}

func Render(path string) string {
Expand Down
85 changes: 71 additions & 14 deletions examples/gno.land/r/jjoptimist/eventix/eventix_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package eventix
import (
"std"
"testing"
"fmt"

"gno.land/p/demo/grc/grc721"
"gno.land/p/demo/uassert"
"gno.land/p/demo/urequire"
"gno.land/p/demo/grc/grc20"
)

func TestCreateEvent(t *testing.T) {
Expand All @@ -20,17 +22,14 @@ func TestCreateEvent(t *testing.T) {
1000000,
)

// Using uassert to perform a soft assertion.
uassert.Equal(t, 1, eventId)
// Convert eventId to int for comparison since CreateEvent returns uint64
uassert.Equal(t, uint64(1), eventId)

event, exists := getEvent(eventId)
// urequire.True stops the test if the event was not created.
urequire.True(t, exists, "Event was not created")

// Soft-assert that the event name is as expected.
uassert.Equal(t, "Test Event", event.name)

// Test that providing an invalid date format causes a panic.
// Test invalid date format
urequire.PanicsWithMessage(t, "Invalid date format. Use: YYYY-MM-DDThh:mm:ssZ", func() {
CreateEvent("Test", "Test", "invalid-date", "ugnot", 100, 1000000)
})
Expand Down Expand Up @@ -69,14 +68,10 @@ func TestBuyTicket(t *testing.T) {
}

// Verify NFT ownership
tokenId := grc721.TokenID("1-1")
owner, err := tickets.OwnerOf(tokenId) // Handle both return values
if err != nil {
t.Errorf("Error getting token owner: %v", err)
}
if owner != buyer {
t.Errorf("Expected ticket owner to be %s, got %s", buyer, owner)
}
tokenId := grc721.TokenID(fmt.Sprintf("event_%d_ticket_%d", eventId, 1))
owner, err := tickets.OwnerOf(tokenId)
urequire.NoError(t, err)
urequire.Equal(t, buyer, owner)

// Test buying sold out event
std.TestSetOrigSend(std.NewCoins(std.NewCoin("ugnot", 1000000)), std.Coins{})
Expand All @@ -88,3 +83,65 @@ func TestBuyTicket(t *testing.T) {
}()
BuyTicket(eventId) // Should panic - sold out
}

func TestBuyTicketWithGRC20(t *testing.T) {
// Set up package address first
pkgAddr := std.GetOrigPkgAddr()
std.TestSetOrigPkgAddr(pkgAddr)

// Create a test GRC20 token and set up addresses
token, ledger := grc20.NewToken("Test Token", "TEST", 6)
buyer := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")
adminAddr := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf6")

// Set up admin and mint tokens
std.TestSetOrigCaller(adminAddr)
std.TestSetRealm(std.NewUserRealm(adminAddr))
err := ledger.Mint(buyer, 2000000) // Use ledger to mint
urequire.NoError(t, err)

// Create event with GRC20 token payment
std.TestSetOrigCaller(buyer)
std.TestSetRealm(std.NewUserRealm(buyer))

eventId := CreateEvent(
"GRC20 Event",
"An event with GRC20 payment",
"2024-12-31T23:59:59Z",
token.RealmTeller(),
2,
1000000,
)

// Set up for approval
std.TestSetOrigCaller(buyer)
std.TestSetRealm(std.NewUserRealm(buyer))

// Approve using the ledger (following grc20factory_test pattern)
err = ledger.Approve(buyer, pkgAddr, 1000000)
urequire.NoError(t, err)

// Verify approval
allowance := token.Allowance(buyer, pkgAddr)
urequire.Equal(t, uint64(1000000), allowance, "Approval should be set correctly")

// Buy ticket
std.TestSetOrigCaller(buyer)
std.TestSetRealm(std.NewUserRealm(buyer))
BuyTicket(eventId)

// Verify purchase
event, exists := getEvent(eventId)
urequire.True(t, exists)
urequire.Equal(t, 1, event.ticketsSold)

// Verify NFT ownership
tokenId := grc721.TokenID(fmt.Sprintf("event_%d_ticket_%d", eventId, 1))
owner, err := tickets.OwnerOf(tokenId)
urequire.NoError(t, err)
urequire.Equal(t, buyer, owner)

// Verify GRC20 balance changes
buyerBalance := token.BalanceOf(buyer)
urequire.Equal(t, uint64(1000000), buyerBalance) // Should have 1M left after spending 1M
}

0 comments on commit 4c81209

Please sign in to comment.