From 4c8120947c6c609888a5bbe6a8ab9ce992310cf7 Mon Sep 17 00:00:00 2001 From: JJOptimist Date: Tue, 25 Feb 2025 19:46:01 +0100 Subject: [PATCH] fix: GRC20 token payments and NFT token IDs --- .../gno.land/r/jjoptimist/eventix/eventix.gno | 64 +++++++++++--- .../r/jjoptimist/eventix/eventix_test.gno | 85 ++++++++++++++++--- 2 files changed, 122 insertions(+), 27 deletions(-) diff --git a/examples/gno.land/r/jjoptimist/eventix/eventix.gno b/examples/gno.land/r/jjoptimist/eventix/eventix.gno index 862a473a69a..d22a8615c29 100644 --- a/examples/gno.land/r/jjoptimist/eventix/eventix.gno +++ b/examples/gno.land/r/jjoptimist/eventix/eventix.gno @@ -4,11 +4,13 @@ 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 { @@ -16,8 +18,8 @@ type Event struct { 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 } @@ -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, @@ -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 } @@ -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 { diff --git a/examples/gno.land/r/jjoptimist/eventix/eventix_test.gno b/examples/gno.land/r/jjoptimist/eventix/eventix_test.gno index c70d1c82b48..71796e42770 100644 --- a/examples/gno.land/r/jjoptimist/eventix/eventix_test.gno +++ b/examples/gno.land/r/jjoptimist/eventix/eventix_test.gno @@ -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) { @@ -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) }) @@ -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{}) @@ -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 +}