diff --git a/examples/gno.land/p/demo/btree/btree_test.gno b/examples/gno.land/p/demo/btree/btree_test.gno index 871e8c25e1d..959fa7a4254 100644 --- a/examples/gno.land/p/demo/btree/btree_test.gno +++ b/examples/gno.land/p/demo/btree/btree_test.gno @@ -523,91 +523,14 @@ func TestBTree(t *testing.T) { } } -func TestStress(t *testing.T) { - // Loop through creating B-Trees with a range of degrees from 3 to 12, stepping by 3. - // Insert 1000 records into each tree, then search for each record. - // Delete half of the records, skipping every other one, then search for each record. - - for degree := 3; degree <= 12; degree += 3 { - t.Logf("Testing B-Tree of degree %d\n", degree) - tree := New(WithDegree(degree)) - - // Insert 1000 records - t.Logf("Inserting 1000 records\n") - for i := 0; i < 1000; i++ { - content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} - tree.Insert(content) - } - - // Search for all records - for i := 0; i < 1000; i++ { - content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} - val := tree.Get(content) - if val == nil { - t.Errorf("Expected key %v, but didn't find it", content.Key) - } - } - - // Delete half of the records - for i := 0; i < 1000; i += 2 { - content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} - tree.Delete(content) - } - - // Search for all records - for i := 0; i < 1000; i++ { - content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} - val := tree.Get(content) - if i%2 == 0 { - if val != nil { - t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, val.(Content).Key, val.(Content).Value) - } - } else { - if val == nil { - t.Errorf("Expected key %v, but didn't find it", content.Key) - } - } - } - } - - // Now create a very large tree, with 100000 records - // Then delete roughly one third of them, using a very basic random number generation scheme - // (implement it right here) to determine which records to delete. - // Print a few lines using Logf to let the user know what's happening. - - t.Logf("Testing B-Tree of degree 10 with 100000 records\n") - tree := New(WithDegree(10)) - - // Insert 100000 records - t.Logf("Inserting 100000 records\n") - for i := 0; i < 100000; i++ { - content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} - tree.Insert(content) - } - - // Implement a very basic random number generator - seed := 0 - random := func() int { - seed = (seed*1103515245 + 12345) & 0x7fffffff - return seed - } - - // Delete one third of the records - t.Logf("Deleting one third of the records\n") - for i := 0; i < 35000; i++ { - content := Content{Key: random() % 100000, Value: fmt.Sprintf("Value_%d", i)} - tree.Delete(content) - } -} - -// Write a test that populates a large B-Tree with 10000 records. +// Write a test that populates a large B-Tree with 1000 records. // It should then `Clone` the tree, make some changes to both the original and the clone, // And then clone the clone, and make some changes to all three trees, and then check that the changes are isolated // to the tree they were made in. - func TestBTreeCloneIsolation(t *testing.T) { - t.Logf("Creating B-Tree of degree 10 with 10000 records\n") - tree := genericSeeding(New(WithDegree(10)), 10000) + t.Logf("Creating B-Tree of degree 10 with 1000 records\n") + size := 1000 + tree := genericSeeding(New(WithDegree(10)), size) // Clone the tree t.Logf("Cloning the tree\n") @@ -615,7 +538,7 @@ func TestBTreeCloneIsolation(t *testing.T) { // Make some changes to the original and the clone t.Logf("Making changes to the original and the clone\n") - for i := 0; i < 10000; i += 2 { + for i := 0; i < size; i += 2 { content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} tree.Delete(content) content = Content{Key: i + 1, Value: fmt.Sprintf("Value_%d", i+1)} @@ -628,7 +551,7 @@ func TestBTreeCloneIsolation(t *testing.T) { // Make some changes to all three trees t.Logf("Making changes to all three trees\n") - for i := 0; i < 10000; i += 3 { + for i := 0; i < size; i += 3 { content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} tree.Delete(content) content = Content{Key: i, Value: fmt.Sprintf("Value_%d", i+1)} @@ -639,7 +562,7 @@ func TestBTreeCloneIsolation(t *testing.T) { // Check that the changes are isolated to the tree they were made in t.Logf("Checking that the changes are isolated to the tree they were made in\n") - for i := 0; i < 10000; i++ { + for i := 0; i < size; i++ { content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} val := tree.Get(content) @@ -676,3 +599,83 @@ func TestBTreeCloneIsolation(t *testing.T) { } } } + +// -------------------- +// Stress tests. Disabled for testing performance + +//func TestStress(t *testing.T) { +// // Loop through creating B-Trees with a range of degrees from 3 to 12, stepping by 3. +// // Insert 1000 records into each tree, then search for each record. +// // Delete half of the records, skipping every other one, then search for each record. +// +// for degree := 3; degree <= 12; degree += 3 { +// t.Logf("Testing B-Tree of degree %d\n", degree) +// tree := New(WithDegree(degree)) +// +// // Insert 1000 records +// t.Logf("Inserting 1000 records\n") +// for i := 0; i < 1000; i++ { +// content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} +// tree.Insert(content) +// } +// +// // Search for all records +// for i := 0; i < 1000; i++ { +// content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} +// val := tree.Get(content) +// if val == nil { +// t.Errorf("Expected key %v, but didn't find it", content.Key) +// } +// } +// +// // Delete half of the records +// for i := 0; i < 1000; i += 2 { +// content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} +// tree.Delete(content) +// } +// +// // Search for all records +// for i := 0; i < 1000; i++ { +// content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} +// val := tree.Get(content) +// if i%2 == 0 { +// if val != nil { +// t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, val.(Content).Key, val.(Content).Value) +// } +// } else { +// if val == nil { +// t.Errorf("Expected key %v, but didn't find it", content.Key) +// } +// } +// } +// } +// +// // Now create a very large tree, with 100000 records +// // Then delete roughly one third of them, using a very basic random number generation scheme +// // (implement it right here) to determine which records to delete. +// // Print a few lines using Logf to let the user know what's happening. +// +// t.Logf("Testing B-Tree of degree 10 with 100000 records\n") +// tree := New(WithDegree(10)) +// +// // Insert 100000 records +// t.Logf("Inserting 100000 records\n") +// for i := 0; i < 100000; i++ { +// content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} +// tree.Insert(content) +// } +// +// // Implement a very basic random number generator +// seed := 0 +// random := func() int { +// seed = (seed*1103515245 + 12345) & 0x7fffffff +// return seed +// } +// +// // Delete one third of the records +// t.Logf("Deleting one third of the records\n") +// for i := 0; i < 35000; i++ { +// content := Content{Key: random() % 100000, Value: fmt.Sprintf("Value_%d", i)} +// tree.Delete(content) +// } +//} diff --git a/examples/gno.land/p/demo/diff/diff_test.gno b/examples/gno.land/p/demo/diff/diff_test.gno index bbf4fcdf3e0..3993c91664a 100644 --- a/examples/gno.land/p/demo/diff/diff_test.gno +++ b/examples/gno.land/p/demo/diff/diff_test.gno @@ -162,12 +162,13 @@ func TestMyersDiff(t *testing.T) { new: strings.Repeat("b", 1000), expected: "[-" + strings.Repeat("a", 1000) + "][+" + strings.Repeat("b", 1000) + "]", }, - { - name: "Very long strings", - old: strings.Repeat("a", 10000) + "b" + strings.Repeat("a", 10000), - new: strings.Repeat("a", 10000) + "c" + strings.Repeat("a", 10000), - expected: strings.Repeat("a", 10000) + "[-b][+c]" + strings.Repeat("a", 10000), - }, + //{ // disabled for testing performance + // XXX: consider adding a flag to run such tests, not like `-short`, or switching to a `-bench`, maybe. + // name: "Very long strings", + // old: strings.Repeat("a", 10000) + "b" + strings.Repeat("a", 10000), + // new: strings.Repeat("a", 10000) + "c" + strings.Repeat("a", 10000), + // expected: strings.Repeat("a", 10000) + "[-b][+c]" + strings.Repeat("a", 10000), + //}, } for _, tc := range tests { diff --git a/examples/gno.land/p/demo/json/node_test.gno b/examples/gno.land/p/demo/json/node_test.gno index dbc82369f68..c364187ac86 100644 --- a/examples/gno.land/p/demo/json/node_test.gno +++ b/examples/gno.land/p/demo/json/node_test.gno @@ -285,7 +285,7 @@ func TestNode_GetBool(t *testing.T) { } } -func TestNode_GetBool_Fail(t *testing.T) { +func TestNode_GetBool_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"literally null node", NullNode("")}, @@ -357,7 +357,7 @@ func TestNode_GetNull(t *testing.T) { } } -func TestNode_GetNull_Fail(t *testing.T) { +func TestNode_GetNull_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"number node is null", NumberNode("", 42)}, @@ -435,7 +435,7 @@ func TestNode_GetNumeric_With_Unmarshal(t *testing.T) { } } -func TestNode_GetNumeric_Fail(t *testing.T) { +func TestNode_GetNumeric_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"null node", NullNode("")}, @@ -467,7 +467,7 @@ func TestNode_GetString(t *testing.T) { } } -func TestNode_GetString_Fail(t *testing.T) { +func TestNode_GetString_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"null node", NullNode("")}, @@ -577,7 +577,7 @@ func TestNode_GetArray(t *testing.T) { } } -func TestNode_GetArray_Fail(t *testing.T) { +func TestNode_GetArray_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"null node", NullNode("")}, @@ -736,7 +736,7 @@ func TestNode_Index(t *testing.T) { } } -func TestNode_Index_Fail(t *testing.T) { +func TestNode_Index_NotSucceed(t *testing.T) { tests := []struct { name string node *Node @@ -854,7 +854,7 @@ func TestNode_GetKey(t *testing.T) { } } -func TestNode_GetKey_Fail(t *testing.T) { +func TestNode_GetKey_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"null node", NullNode("")}, @@ -998,7 +998,7 @@ func TestNode_GetObject(t *testing.T) { } } -func TestNode_GetObject_Fail(t *testing.T) { +func TestNode_GetObject_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"get object from null node", NullNode("")}, diff --git a/examples/gno.land/p/demo/ownable/ownable.gno b/examples/gno.land/p/demo/ownable/ownable.gno index f565e27c0f2..a8cb5ea95a7 100644 --- a/examples/gno.land/p/demo/ownable/ownable.gno +++ b/examples/gno.land/p/demo/ownable/ownable.gno @@ -65,18 +65,28 @@ func (o *Ownable) DropOwnership() error { } // Owner returns the owner address from Ownable -func (o Ownable) Owner() std.Address { +func (o *Ownable) Owner() std.Address { + if o == nil { + return std.Address("") + } return o.owner } // CallerIsOwner checks if the caller of the function is the Realm's owner -func (o Ownable) CallerIsOwner() bool { +func (o *Ownable) CallerIsOwner() bool { + if o == nil { + return false + } return std.PrevRealm().Addr() == o.owner } // AssertCallerIsOwner panics if the caller is not the owner -func (o Ownable) AssertCallerIsOwner() { - if std.PrevRealm().Addr() != o.owner { +func (o *Ownable) AssertCallerIsOwner() { + if o == nil { + panic(ErrUnauthorized) + } + caller := std.PrevRealm().Addr() + if caller != o.owner { panic(ErrUnauthorized) } } diff --git a/examples/gno.land/p/demo/ownable/ownable_test.gno b/examples/gno.land/p/demo/ownable/ownable_test.gno index f58af9642c6..d8b7f9a8e3a 100644 --- a/examples/gno.land/p/demo/ownable/ownable_test.gno +++ b/examples/gno.land/p/demo/ownable/ownable_test.gno @@ -93,3 +93,51 @@ func TestErrInvalidAddress(t *testing.T) { err = o.TransferOwnership("10000000001000000000100000000010000000001000000000") uassert.ErrorContains(t, err, ErrInvalidAddress.Error()) } + +func TestAssertCallerIsOwner(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + + // Should not panic when caller is owner + o.AssertCallerIsOwner() + + // Should panic when caller is not owner + std.TestSetRealm(std.NewUserRealm(bob)) + std.TestSetOrigCaller(bob) + + defer func() { + r := recover() + if r == nil { + t.Error("expected panic but got none") + } + if r != ErrUnauthorized { + t.Errorf("expected ErrUnauthorized but got %v", r) + } + }() + o.AssertCallerIsOwner() +} + +func TestNilReceiver(t *testing.T) { + var o *Ownable + + owner := o.Owner() + if owner != std.Address("") { + t.Errorf("expected empty address but got %v", owner) + } + + isOwner := o.CallerIsOwner() + uassert.False(t, isOwner) + + defer func() { + r := recover() + if r == nil { + t.Error("expected panic but got none") + } + if r != ErrUnauthorized { + t.Errorf("expected ErrUnauthorized but got %v", r) + } + }() + o.AssertCallerIsOwner() +} diff --git a/examples/gno.land/p/demo/pausable/pausable.gno b/examples/gno.land/p/demo/pausable/pausable.gno index e6a85771fa6..fa3962cab41 100644 --- a/examples/gno.land/p/demo/pausable/pausable.gno +++ b/examples/gno.land/p/demo/pausable/pausable.gno @@ -7,23 +7,15 @@ import ( ) type Pausable struct { - *ownable.Ownable + o *ownable.Ownable paused bool } -// New returns a new Pausable struct with non-paused state as default -func New() *Pausable { - return &Pausable{ - Ownable: ownable.New(), - paused: false, - } -} - // NewFromOwnable is the same as New, but with a pre-existing top-level ownable func NewFromOwnable(ownable *ownable.Ownable) *Pausable { return &Pausable{ - Ownable: ownable, - paused: false, + o: ownable, + paused: false, } } @@ -34,24 +26,29 @@ func (p Pausable) IsPaused() bool { // Pause sets the state of Pausable to true, meaning all pausable functions are paused func (p *Pausable) Pause() error { - if !p.CallerIsOwner() { + if !p.o.CallerIsOwner() { return ownable.ErrUnauthorized } p.paused = true - std.Emit("Paused", "account", p.Owner().String()) + std.Emit("Paused", "account", p.o.Owner().String()) return nil } // Unpause sets the state of Pausable to false, meaning all pausable functions are resumed func (p *Pausable) Unpause() error { - if !p.CallerIsOwner() { + if !p.o.CallerIsOwner() { return ownable.ErrUnauthorized } p.paused = false - std.Emit("Unpaused", "account", p.Owner().String()) + std.Emit("Unpaused", "account", p.o.Owner().String()) return nil } + +// Ownable returns the underlying ownable +func (p *Pausable) Ownable() *ownable.Ownable { + return p.o +} diff --git a/examples/gno.land/p/demo/pausable/pausable_test.gno b/examples/gno.land/p/demo/pausable/pausable_test.gno index c9557245bdf..47028cd85c8 100644 --- a/examples/gno.land/p/demo/pausable/pausable_test.gno +++ b/examples/gno.land/p/demo/pausable/pausable_test.gno @@ -5,57 +5,49 @@ import ( "testing" "gno.land/p/demo/ownable" + "gno.land/p/demo/uassert" "gno.land/p/demo/urequire" ) var ( - firstCaller = std.Address("g1l9aypkr8xfvs82zeux486ddzec88ty69lue9de") - secondCaller = std.Address("g127jydsh6cms3lrtdenydxsckh23a8d6emqcvfa") + firstCaller = std.Address("g1l9aypkr8xfvs82zeux486ddzec88ty69lue9de") + o = ownable.NewWithAddress(firstCaller) ) -func TestNew(t *testing.T) { - std.TestSetOrigCaller(firstCaller) - - result := New() - - urequire.False(t, result.paused, "Expected result to be unpaused") - urequire.Equal(t, firstCaller.String(), result.Owner().String()) -} - func TestNewFromOwnable(t *testing.T) { std.TestSetOrigCaller(firstCaller) - o := ownable.New() - std.TestSetOrigCaller(secondCaller) result := NewFromOwnable(o) - - urequire.Equal(t, firstCaller.String(), result.Owner().String()) + urequire.Equal(t, firstCaller.String(), result.Ownable().Owner().String()) } func TestSetUnpaused(t *testing.T) { std.TestSetOrigCaller(firstCaller) + result := NewFromOwnable(o) - result := New() result.Unpause() - - urequire.False(t, result.IsPaused(), "Expected result to be unpaused") + uassert.False(t, result.IsPaused(), "Expected result to be unpaused") } func TestSetPaused(t *testing.T) { std.TestSetOrigCaller(firstCaller) + result := NewFromOwnable(o) - result := New() result.Pause() - - urequire.True(t, result.IsPaused(), "Expected result to be paused") + uassert.True(t, result.IsPaused(), "Expected result to be paused") } func TestIsPaused(t *testing.T) { - std.TestSetOrigCaller(firstCaller) - - result := New() + result := NewFromOwnable(o) urequire.False(t, result.IsPaused(), "Expected result to be unpaused") + std.TestSetOrigCaller(firstCaller) result.Pause() - urequire.True(t, result.IsPaused(), "Expected result to be paused") + uassert.True(t, result.IsPaused(), "Expected result to be paused") +} + +func TestOwnable(t *testing.T) { + result := NewFromOwnable(o) + + uassert.Equal(t, result.Ownable().Owner(), o.Owner()) } diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno index 46251e24dad..275455d1479 100644 --- a/examples/gno.land/p/demo/simpledao/dao_test.gno +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -752,7 +752,7 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { dao.ExecutionSuccessful, }, { - "execution failed", + "execution not succeeded", dao.ExecutionFailed, }, } diff --git a/examples/gno.land/p/moul/helplink/helplink_test.gno b/examples/gno.land/p/moul/helplink/helplink_test.gno index 29cfd02eb67..440001b94ce 100644 --- a/examples/gno.land/p/moul/helplink/helplink_test.gno +++ b/examples/gno.land/p/moul/helplink/helplink_test.gno @@ -18,7 +18,7 @@ func TestFunc(t *testing.T) { {"Realm Example", "foo", []string{"bar", "1", "baz", "2"}, "[Realm Example](/r/lorem/ipsum$help&func=foo&bar=1&baz=2)", "gno.land/r/lorem/ipsum"}, {"Single Arg", "testFunc", []string{"key", "value"}, "[Single Arg]($help&func=testFunc&key=value)", ""}, {"No Args", "noArgsFunc", []string{}, "[No Args]($help&func=noArgsFunc)", ""}, - {"Odd Args", "oddArgsFunc", []string{"key"}, "[Odd Args]($help&func=oddArgsFunc)", ""}, + {"Odd Args", "oddArgsFunc", []string{"key"}, "[Odd Args]($help&func=oddArgsFunc&error=odd+number+of+arguments)", ""}, } for _, tt := range tests { @@ -39,15 +39,15 @@ func TestFuncURL(t *testing.T) { {"foo", []string{"bar", "1", "baz", "2"}, "$help&func=foo&bar=1&baz=2", ""}, {"testFunc", []string{"key", "value"}, "$help&func=testFunc&key=value", ""}, {"noArgsFunc", []string{}, "$help&func=noArgsFunc", ""}, - {"oddArgsFunc", []string{"key"}, "$help&func=oddArgsFunc", ""}, + {"oddArgsFunc", []string{"key"}, "$help&func=oddArgsFunc&error=odd+number+of+arguments", ""}, {"foo", []string{"bar", "1", "baz", "2"}, "/r/lorem/ipsum$help&func=foo&bar=1&baz=2", "gno.land/r/lorem/ipsum"}, {"testFunc", []string{"key", "value"}, "/r/lorem/ipsum$help&func=testFunc&key=value", "gno.land/r/lorem/ipsum"}, {"noArgsFunc", []string{}, "/r/lorem/ipsum$help&func=noArgsFunc", "gno.land/r/lorem/ipsum"}, - {"oddArgsFunc", []string{"key"}, "/r/lorem/ipsum$help&func=oddArgsFunc", "gno.land/r/lorem/ipsum"}, + {"oddArgsFunc", []string{"key"}, "/r/lorem/ipsum$help&func=oddArgsFunc&error=odd+number+of+arguments", "gno.land/r/lorem/ipsum"}, {"foo", []string{"bar", "1", "baz", "2"}, "https://gno.world/r/lorem/ipsum$help&func=foo&bar=1&baz=2", "gno.world/r/lorem/ipsum"}, {"testFunc", []string{"key", "value"}, "https://gno.world/r/lorem/ipsum$help&func=testFunc&key=value", "gno.world/r/lorem/ipsum"}, {"noArgsFunc", []string{}, "https://gno.world/r/lorem/ipsum$help&func=noArgsFunc", "gno.world/r/lorem/ipsum"}, - {"oddArgsFunc", []string{"key"}, "https://gno.world/r/lorem/ipsum$help&func=oddArgsFunc", "gno.world/r/lorem/ipsum"}, + {"oddArgsFunc", []string{"key"}, "https://gno.world/r/lorem/ipsum$help&func=oddArgsFunc&error=odd+number+of+arguments", "gno.world/r/lorem/ipsum"}, } for _, tt := range tests { diff --git a/examples/gno.land/p/moul/txlink/txlink.gno b/examples/gno.land/p/moul/txlink/txlink.gno index 65edda6911e..8f753b4546d 100644 --- a/examples/gno.land/p/moul/txlink/txlink.gno +++ b/examples/gno.land/p/moul/txlink/txlink.gno @@ -15,6 +15,7 @@ package txlink import ( + "net/url" "std" "strings" ) @@ -51,24 +52,26 @@ func (r Realm) prefix() string { // Call returns a URL for the specified function with optional key-value // arguments. func (r Realm) Call(fn string, args ...string) string { - // Start with the base query - url := r.prefix() + "$help&func=" + fn + if len(args) == 0 { + return r.prefix() + "$help&func=" + fn + } + + // Create url.Values to properly encode parameters. + // But manage &func=fn as a special case to keep it as the first argument. + values := url.Values{} // Check if args length is even if len(args)%2 != 0 { - // If not even, we can choose to handle the error here. - // For example, we can just return the URL without appending - // more args. - return url - } - - // Append key-value pairs to the URL - for i := 0; i < len(args); i += 2 { - key := args[i] - value := args[i+1] - // XXX: escape keys and args - url += "&" + key + "=" + value + values.Add("error", "odd number of arguments") + } else { + // Add key-value pairs to values + for i := 0; i < len(args); i += 2 { + key := args[i] + value := args[i+1] + values.Add(key, value) + } } - return url + // Build the base URL and append encoded query parameters + return r.prefix() + "$help&func=" + fn + "&" + values.Encode() } diff --git a/examples/gno.land/p/moul/txlink/txlink_test.gno b/examples/gno.land/p/moul/txlink/txlink_test.gno index 61b532270d4..a43bb14c261 100644 --- a/examples/gno.land/p/moul/txlink/txlink_test.gno +++ b/examples/gno.land/p/moul/txlink/txlink_test.gno @@ -16,19 +16,24 @@ func TestCall(t *testing.T) { {"foo", []string{"bar", "1", "baz", "2"}, "$help&func=foo&bar=1&baz=2", ""}, {"testFunc", []string{"key", "value"}, "$help&func=testFunc&key=value", ""}, {"noArgsFunc", []string{}, "$help&func=noArgsFunc", ""}, - {"oddArgsFunc", []string{"key"}, "$help&func=oddArgsFunc", ""}, + {"oddArgsFunc", []string{"key"}, "$help&func=oddArgsFunc&error=odd+number+of+arguments", ""}, {"foo", []string{"bar", "1", "baz", "2"}, "/r/lorem/ipsum$help&func=foo&bar=1&baz=2", "gno.land/r/lorem/ipsum"}, {"testFunc", []string{"key", "value"}, "/r/lorem/ipsum$help&func=testFunc&key=value", "gno.land/r/lorem/ipsum"}, {"noArgsFunc", []string{}, "/r/lorem/ipsum$help&func=noArgsFunc", "gno.land/r/lorem/ipsum"}, - {"oddArgsFunc", []string{"key"}, "/r/lorem/ipsum$help&func=oddArgsFunc", "gno.land/r/lorem/ipsum"}, + {"oddArgsFunc", []string{"key"}, "/r/lorem/ipsum$help&func=oddArgsFunc&error=odd+number+of+arguments", "gno.land/r/lorem/ipsum"}, {"foo", []string{"bar", "1", "baz", "2"}, "https://gno.world/r/lorem/ipsum$help&func=foo&bar=1&baz=2", "gno.world/r/lorem/ipsum"}, {"testFunc", []string{"key", "value"}, "https://gno.world/r/lorem/ipsum$help&func=testFunc&key=value", "gno.world/r/lorem/ipsum"}, {"noArgsFunc", []string{}, "https://gno.world/r/lorem/ipsum$help&func=noArgsFunc", "gno.world/r/lorem/ipsum"}, - {"oddArgsFunc", []string{"key"}, "https://gno.world/r/lorem/ipsum$help&func=oddArgsFunc", "gno.world/r/lorem/ipsum"}, + {"oddArgsFunc", []string{"key"}, "https://gno.world/r/lorem/ipsum$help&func=oddArgsFunc&error=odd+number+of+arguments", "gno.world/r/lorem/ipsum"}, + {"foo", []string{"bar", "1"}, "$help&func=foo&bar=1", ""}, + {"test", []string{"key", "hello world"}, "$help&func=test&key=hello+world", ""}, + {"test", []string{"key", "a&b=c"}, "$help&func=test&key=a%26b%3Dc", ""}, + {"test", []string{"key", ""}, "$help&func=test&key=", ""}, + {"test", []string{"key", "1", "key", "2"}, "$help&func=test&key=1&key=2", ""}, } for _, tt := range tests { - title := tt.fn + title := string(tt.realm) + "_" + tt.fn t.Run(title, func(t *testing.T) { got := tt.realm.Call(tt.fn, tt.args...) urequire.Equal(t, tt.want, got) diff --git a/examples/gno.land/r/demo/boards/z_0_filetest.gno b/examples/gno.land/r/demo/boards/z_0_filetest.gno index a649895cb01..f56f6495b17 100644 --- a/examples/gno.land/r/demo/boards/z_0_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_filetest.gno @@ -30,12 +30,12 @@ func main() { // ## [First Post (title)](/r/demo/boards:test_board/1) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (0 reposts) +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] (0 replies) (0 reposts) // // ---------------------------------------- // ## [Second Post (title)](/r/demo/boards:test_board/2) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/2) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] (1 replies) (0 reposts) +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/2) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=2)] (1 replies) (0 reposts) // // diff --git a/examples/gno.land/r/demo/boards/z_10_c_filetest.gno b/examples/gno.land/r/demo/boards/z_10_c_filetest.gno index 7dd460500d6..3fdd915a389 100644 --- a/examples/gno.land/r/demo/boards/z_10_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_c_filetest.gno @@ -35,15 +35,15 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // // > First reply of the First post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=1)] // // ---------------------------------------------------- // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // diff --git a/examples/gno.land/r/demo/boards/z_10_filetest.gno b/examples/gno.land/r/demo/boards/z_10_filetest.gno index 8a6d11c79cf..80254592d5f 100644 --- a/examples/gno.land/r/demo/boards/z_10_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_filetest.gno @@ -33,7 +33,7 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // // ---------------------------------------------------- // thread does not exist with id: 1 diff --git a/examples/gno.land/r/demo/boards/z_11_d_filetest.gno b/examples/gno.land/r/demo/boards/z_11_d_filetest.gno index f64b4c84bba..0a5c1886d24 100644 --- a/examples/gno.land/r/demo/boards/z_11_d_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_d_filetest.gno @@ -35,19 +35,19 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // // > First reply of the First post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=1)] // // ---------------------------------------------------- // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // // > Edited: First reply of the First post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=1)] // diff --git a/examples/gno.land/r/demo/boards/z_11_filetest.gno b/examples/gno.land/r/demo/boards/z_11_filetest.gno index 3f56293b3bd..1f04d7b686d 100644 --- a/examples/gno.land/r/demo/boards/z_11_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_filetest.gno @@ -33,11 +33,11 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // // ---------------------------------------------------- // # Edited: First Post in (title) // // Edited: Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // diff --git a/examples/gno.land/r/demo/boards/z_12_filetest.gno b/examples/gno.land/r/demo/boards/z_12_filetest.gno index ac4adf6ee7b..a362ea59823 100644 --- a/examples/gno.land/r/demo/boards/z_12_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_filetest.gno @@ -37,6 +37,6 @@ func main() { // ## [First Post (title)](/r/demo/boards:test_board1/1) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board1/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (1 reposts) +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board1/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] (0 replies) (1 reposts) // // diff --git a/examples/gno.land/r/demo/boards/z_2_filetest.gno b/examples/gno.land/r/demo/boards/z_2_filetest.gno index 31b39644b24..55f94e10f1a 100644 --- a/examples/gno.land/r/demo/boards/z_2_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_2_filetest.gno @@ -32,8 +32,8 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // diff --git a/examples/gno.land/r/demo/boards/z_3_filetest.gno b/examples/gno.land/r/demo/boards/z_3_filetest.gno index 0b2a2df2f91..c2aa264b1d2 100644 --- a/examples/gno.land/r/demo/boards/z_3_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_3_filetest.gno @@ -34,8 +34,8 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // diff --git a/examples/gno.land/r/demo/boards/z_4_filetest.gno b/examples/gno.land/r/demo/boards/z_4_filetest.gno index b781e94e4db..aede35077f9 100644 --- a/examples/gno.land/r/demo/boards/z_4_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_4_filetest.gno @@ -37,13 +37,13 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // // > Second reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=4&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=4&threadid=2)] // // Realm: diff --git a/examples/gno.land/r/demo/boards/z_5_c_filetest.gno b/examples/gno.land/r/demo/boards/z_5_c_filetest.gno index 723e6a10204..176c11da73c 100644 --- a/examples/gno.land/r/demo/boards/z_5_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_c_filetest.gno @@ -33,8 +33,8 @@ func main() { // # First Post (title) // // Body of the first post. (body) -// \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // // > Reply of the first post -// > \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] +// > \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=1)] // diff --git a/examples/gno.land/r/demo/boards/z_5_filetest.gno b/examples/gno.land/r/demo/boards/z_5_filetest.gno index 712af483891..4f6f5cb9b75 100644 --- a/examples/gno.land/r/demo/boards/z_5_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_filetest.gno @@ -33,12 +33,12 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // // > Second reply of the second post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=4&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=4&threadid=2)] // diff --git a/examples/gno.land/r/demo/boards/z_6_filetest.gno b/examples/gno.land/r/demo/boards/z_6_filetest.gno index ec40cf5f8e9..39791606aae 100644 --- a/examples/gno.land/r/demo/boards/z_6_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_6_filetest.gno @@ -35,16 +35,16 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // > // > > First reply of the first reply // > > -// > > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=5)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=5)] +// > > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=5&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=5&threadid=2)] // // > Second reply of the second post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=4&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=4&threadid=2)] // diff --git a/examples/gno.land/r/demo/boards/z_7_filetest.gno b/examples/gno.land/r/demo/boards/z_7_filetest.gno index 353b84f6d87..9ff95f7492d 100644 --- a/examples/gno.land/r/demo/boards/z_7_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_7_filetest.gno @@ -28,6 +28,6 @@ func main() { // ## [First Post (title)](/r/demo/boards:test_board/1) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (0 reposts) +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] (0 replies) (0 reposts) // // diff --git a/examples/gno.land/r/demo/boards/z_8_filetest.gno b/examples/gno.land/r/demo/boards/z_8_filetest.gno index 4896dfcfccf..47d84737698 100644 --- a/examples/gno.land/r/demo/boards/z_8_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_8_filetest.gno @@ -35,11 +35,11 @@ func main() { // _[see thread](/r/demo/boards:test_board/2)_ // // Reply of the second post -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // // _[see all 1 replies](/r/demo/boards:test_board/2/3)_ // // > First reply of the first reply // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=5)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=5)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=5&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=5&threadid=2)] // diff --git a/examples/gno.land/r/demo/boards/z_9_filetest.gno b/examples/gno.land/r/demo/boards/z_9_filetest.gno index ca37e306bda..96af90263d5 100644 --- a/examples/gno.land/r/demo/boards/z_9_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_9_filetest.gno @@ -34,5 +34,5 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:second_board/1/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=2&threadid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=2&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:second_board/1/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=2&postid=1&threadid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=2&postid=1&threadid=1)] // diff --git a/examples/gno.land/r/docs/buttons/buttons_test.gno b/examples/gno.land/r/docs/buttons/buttons_test.gno index 2903fa1a858..c6164f3c687 100644 --- a/examples/gno.land/r/docs/buttons/buttons_test.gno +++ b/examples/gno.land/r/docs/buttons/buttons_test.gno @@ -7,7 +7,7 @@ import ( func TestRenderMotdLink(t *testing.T) { res := Render("motd") - const wantLink = "/r/docs/buttons$help&func=UpdateMOTD&newmotd=Message!" + const wantLink = "/r/docs/buttons$help&func=UpdateMOTD&newmotd=Message%21" if !strings.Contains(res, wantLink) { t.Fatalf("%s\ndoes not contain correct help page link: %s", res, wantLink) } diff --git a/examples/gno.land/r/leon/hof/datasource_test.gno b/examples/gno.land/r/leon/hof/datasource_test.gno index 376f981875f..fb67f20e7e7 100644 --- a/examples/gno.land/r/leon/hof/datasource_test.gno +++ b/examples/gno.land/r/leon/hof/datasource_test.gno @@ -151,7 +151,7 @@ func TestItemRecord(t *testing.T) { content, _ := r.Content() wantContent := "# Submission #1\n\n\n```\ngno.land/r/demo/test\n```\n\nby demo\n\n" + "[View realm](/r/demo/test)\n\nSubmitted at Block #42\n\n" + - "#### [2👍](/r/leon/hof$help&func=Upvote&pkgpath=gno.land/r/demo/test) - " + - "[1👎](/r/leon/hof$help&func=Downvote&pkgpath=gno.land/r/demo/test)\n\n" + "#### [2👍](/r/leon/hof$help&func=Upvote&pkgpath=gno.land%2Fr%2Fdemo%2Ftest) - " + + "[1👎](/r/leon/hof$help&func=Downvote&pkgpath=gno.land%2Fr%2Fdemo%2Ftest)\n\n" uassert.Equal(t, wantContent, content) } diff --git a/examples/gno.land/r/stefann/fomo3d/errors.gno b/examples/gno.land/r/stefann/fomo3d/errors.gno new file mode 100644 index 00000000000..df70ab08c55 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/errors.gno @@ -0,0 +1,30 @@ +package fomo3d + +import "errors" + +var ( + // Game state errors + ErrGameInProgress = errors.New("fomo3d: game already in progress") + ErrGameNotInProgress = errors.New("fomo3d: game not in progress") + ErrGameEnded = errors.New("fomo3d: game has ended") + ErrGameTimeExpired = errors.New("fomo3d: game time expired") + ErrNoKeysPurchased = errors.New("fomo3d: no keys purchased") + ErrPlayerNotInGame = errors.New("fomo3d: player is not in the game") + + // Payment errors + ErrInvalidPayment = errors.New("fomo3d: must send ugnot only") + ErrInsufficientPayment = errors.New("fomo3d: insufficient payment for key") + + // Dividend errors + ErrNoDividendsToClaim = errors.New("fomo3d: no dividends to claim") + + // Fee errors + ErrNoFeesToClaim = errors.New("fomo3d: no owner fees to claim") + + // Resolution errors + ErrInvalidAddressOrName = errors.New("fomo3d: invalid address or unregistered username") + + // NFT errors + ErrUnauthorizedMint = errors.New("fomo3d: only the Fomo3D game realm can mint winner NFTs") + ErrZeroAddress = errors.New("fomo3d: zero address") +) diff --git a/examples/gno.land/r/stefann/fomo3d/events.gno b/examples/gno.land/r/stefann/fomo3d/events.gno new file mode 100644 index 00000000000..ea404466955 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/events.gno @@ -0,0 +1,94 @@ +package fomo3d + +import ( + "std" + + "gno.land/p/demo/ufmt" +) + +// Event names +const ( + // Game events + GameStartedEvent = "GameStarted" + GameEndedEvent = "GameEnded" + KeysPurchasedEvent = "KeysPurchased" + + // Player events + DividendsClaimedEvent = "DividendsClaimed" + + // Admin events + OwnerFeeClaimedEvent = "OwnerFeeClaimed" +) + +// Event keys +const ( + // Common keys + EventRoundKey = "round" + EventAmountKey = "amount" + + // Game keys + EventStartBlockKey = "startBlock" + EventEndBlockKey = "endBlock" + EventStartingPotKey = "startingPot" + EventWinnerKey = "winner" + EventJackpotKey = "jackpot" + + // Player keys + EventBuyerKey = "buyer" + EventNumKeysKey = "numKeys" + EventPriceKey = "price" + EventJackpotShareKey = "jackpotShare" + EventDividendShareKey = "dividendShare" + EventClaimerKey = "claimer" + + // Admin keys + EventOwnerKey = "owner" + EventPreviousOwnerKey = "previousOwner" + EventNewOwnerKey = "newOwner" +) + +func emitGameStarted(round, startBlock, endBlock, startingPot int64) { + std.Emit( + GameStartedEvent, + EventRoundKey, ufmt.Sprintf("%d", round), + EventStartBlockKey, ufmt.Sprintf("%d", startBlock), + EventEndBlockKey, ufmt.Sprintf("%d", endBlock), + EventStartingPotKey, ufmt.Sprintf("%d", startingPot), + ) +} + +func emitGameEnded(round int64, winner std.Address, jackpot int64) { + std.Emit( + GameEndedEvent, + EventRoundKey, ufmt.Sprintf("%d", round), + EventWinnerKey, winner.String(), + EventJackpotKey, ufmt.Sprintf("%d", jackpot), + ) +} + +func emitKeysPurchased(buyer std.Address, numKeys, price, jackpotShare, dividendShare int64) { + std.Emit( + KeysPurchasedEvent, + EventBuyerKey, buyer.String(), + EventNumKeysKey, ufmt.Sprintf("%d", numKeys), + EventPriceKey, ufmt.Sprintf("%d", price), + EventJackpotShareKey, ufmt.Sprintf("%d", jackpotShare), + EventDividendShareKey, ufmt.Sprintf("%d", dividendShare), + ) +} + +func emitDividendsClaimed(claimer std.Address, amount int64) { + std.Emit( + DividendsClaimedEvent, + EventClaimerKey, claimer.String(), + EventAmountKey, ufmt.Sprintf("%d", amount), + ) +} + +func emitOwnerFeeClaimed(owner std.Address, amount int64) { + std.Emit( + OwnerFeeClaimedEvent, + EventOwnerKey, owner.String(), + EventAmountKey, ufmt.Sprintf("%d", amount), + ) +} diff --git a/examples/gno.land/r/stefann/fomo3d/fomo3d.gno b/examples/gno.land/r/stefann/fomo3d/fomo3d.gno new file mode 100644 index 00000000000..b2384ba07f4 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/fomo3d.gno @@ -0,0 +1,358 @@ +package fomo3d + +import ( + "std" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" + "gno.land/p/demo/ufmt" + + "gno.land/r/demo/users" + "gno.land/r/leon/hof" +) + +// FOMO3D (Fear Of Missing Out 3D) is a blockchain-based game that combines elements +// of a lottery and investment mechanics. Players purchase keys using GNOT tokens, +// where each key purchase: +// - Extends the game timer +// - Increases the key price by 1% +// - Makes the buyer the potential winner of the jackpot +// - Distributes dividends to all key holders +// +// Game Mechanics: +// - The last person to buy a key before the timer expires wins the jackpot (47% of all purchases) +// - Key holders earn dividends from each purchase (28% of all purchases) +// - 20% of purchases go to the next round's starting pot +// - 5% goes to development fee +// - Game ends when the timer expires +// +// Inspired by the original Ethereum FOMO3D game but implemented in Gno. + +const ( + MIN_KEY_PRICE int64 = 100000 // minimum key price in ugnot + TIME_EXTENSION int64 = 86400 // time extension in blocks when new key is bought (~24 hours @ 1s blocks) + + // Distribution percentages (total 100%) + JACKPOT_PERCENT int64 = 47 // 47% goes to jackpot + DIVIDENDS_PERCENT int64 = 28 // 28% distributed to key holders + NEXT_ROUND_POT int64 = 20 // 20% goes to next round's starting pot + OWNER_FEE_PERCENT int64 = 5 // 5% goes to contract owner +) + +type PlayerInfo struct { + Keys int64 // number of keys owned + Dividends int64 // unclaimed dividends in ugnot +} + +// GameState represents the current state of the FOMO3D game +type GameState struct { // TODO: Separate GameState and RoundState and save round history tree in GameState + StartBlock int64 // Block when the game started + EndBlock int64 // Block when the game will end + LastKeyBlock int64 // Block of last key purchase + LastBuyer std.Address // Address of last key buyer + Jackpot int64 // Current jackpot in ugnot + KeyPrice int64 // Current price of keys in ugnot + TotalKeys int64 // Total number of keys in circulation + Ended bool // Whether the game has ended + CurrentRound int64 // Current round number + NextPot int64 // Next round's starting pot + OwnerFee int64 // Accumulated owner fees + BuyKeysLink string // Link to BuyKeys function + ClaimDividendsLink string // Link to ClaimDividends function + StartGameLink string // Link to StartGame function +} + +var ( + gameState GameState + players *avl.Tree // maps address -> PlayerInfo + Ownable *ownable.Ownable +) + +func init() { + Ownable = ownable.New() + players = avl.NewTree() + gameState.Ended = true + hof.Register() +} + +// StartGame starts a new game round +func StartGame() { + if !gameState.Ended && gameState.StartBlock != 0 { + panic(ErrGameInProgress.Error()) + } + + gameState.CurrentRound++ + gameState.StartBlock = std.GetHeight() + gameState.EndBlock = gameState.StartBlock + TIME_EXTENSION // Initial 24h window + gameState.LastKeyBlock = gameState.StartBlock + gameState.Jackpot = gameState.NextPot + gameState.NextPot = 0 + gameState.Ended = false + gameState.KeyPrice = MIN_KEY_PRICE + gameState.TotalKeys = 0 + + // Clear previous round's player data + players = avl.NewTree() + + emitGameStarted( + gameState.CurrentRound, + gameState.StartBlock, + gameState.EndBlock, + gameState.Jackpot, + ) +} + +// BuyKeys allows players to purchase keys +func BuyKeys() { + if gameState.Ended { + panic(ErrGameEnded.Error()) + } + + currentBlock := std.GetHeight() + if currentBlock > gameState.EndBlock { + panic(ErrGameTimeExpired.Error()) + } + + // Get sent coins + sent := std.GetOrigSend() + if len(sent) != 1 || sent[0].Denom != "ugnot" { + panic(ErrInvalidPayment.Error()) + } + + payment := sent.AmountOf("ugnot") + if payment < gameState.KeyPrice { + panic(ErrInsufficientPayment.Error()) + } + + // Calculate number of keys that can be bought and actual cost + numKeys := payment / gameState.KeyPrice + actualCost := numKeys * gameState.KeyPrice + excess := payment - actualCost + + // Update buyer's info + buyer := std.PrevRealm().Addr() + var buyerInfo PlayerInfo + if info, exists := players.Get(buyer.String()); exists { + buyerInfo = info.(PlayerInfo) + } + + buyerInfo.Keys += numKeys + gameState.TotalKeys += numKeys + + // Distribute actual cost + jackpotShare := actualCost * JACKPOT_PERCENT / 100 + dividendShare := actualCost * DIVIDENDS_PERCENT / 100 + nextPotShare := actualCost * NEXT_ROUND_POT / 100 + ownerShare := actualCost * OWNER_FEE_PERCENT / 100 + + // Update pools + gameState.Jackpot += jackpotShare + gameState.NextPot += nextPotShare + gameState.OwnerFee += ownerShare + + // Return excess payment to buyer if any + if excess > 0 { + banker := std.GetBanker(std.BankerTypeOrigSend) + banker.SendCoins( + std.CurrentRealm().Addr(), + buyer, + std.NewCoins(std.NewCoin("ugnot", excess)), + ) + } + + // Distribute dividends to all key holders + if players.Size() > 0 && gameState.TotalKeys > 0 { + dividendPerKey := dividendShare / gameState.TotalKeys + players.Iterate("", "", func(key string, value interface{}) bool { + playerInfo := value.(PlayerInfo) + playerInfo.Dividends += playerInfo.Keys * dividendPerKey + players.Set(key, playerInfo) + return false + }) + } + + // Update game state + gameState.LastBuyer = buyer + gameState.LastKeyBlock = currentBlock + gameState.EndBlock = currentBlock + TIME_EXTENSION // Always extend 24h from current block + gameState.KeyPrice += (gameState.KeyPrice * numKeys) / 100 + + // Save buyer's updated info + players.Set(buyer.String(), buyerInfo) + + emitKeysPurchased( + buyer, + numKeys, + gameState.KeyPrice, + jackpotShare, + dividendShare, + ) +} + +// ClaimDividends allows players to withdraw their earned dividends +func ClaimDividends() { + caller := std.PrevRealm().Addr() + + info, exists := players.Get(caller.String()) + if !exists { + panic(ErrNoDividendsToClaim.Error()) + } + + playerInfo := info.(PlayerInfo) + if playerInfo.Dividends == 0 { + panic(ErrNoDividendsToClaim.Error()) + } + + // Reset dividends and send coins + amount := playerInfo.Dividends + playerInfo.Dividends = 0 + players.Set(caller.String(), playerInfo) + + banker := std.GetBanker(std.BankerTypeRealmSend) + banker.SendCoins( + std.CurrentRealm().Addr(), + caller, + std.NewCoins(std.NewCoin("ugnot", amount)), + ) + + emitDividendsClaimed(caller, amount) +} + +// ClaimOwnerFee allows the owner to withdraw accumulated fees +func ClaimOwnerFee() { + Ownable.AssertCallerIsOwner() + + if gameState.OwnerFee == 0 { + panic(ErrNoFeesToClaim.Error()) + } + + amount := gameState.OwnerFee + gameState.OwnerFee = 0 + + banker := std.GetBanker(std.BankerTypeRealmSend) + banker.SendCoins( + std.CurrentRealm().Addr(), + Ownable.Owner(), + std.NewCoins(std.NewCoin("ugnot", amount)), + ) + + emitOwnerFeeClaimed(Ownable.Owner(), amount) +} + +// EndGame ends the current round and distributes the jackpot +func EndGame() { + if gameState.Ended { + panic(ErrGameEnded.Error()) + } + + currentBlock := std.GetHeight() + if currentBlock <= gameState.EndBlock { + panic(ErrGameNotInProgress.Error()) + } + + if gameState.LastBuyer == "" { + panic(ErrNoKeysPurchased.Error()) + } + + gameState.Ended = true + + // Send jackpot to winner + banker := std.GetBanker(std.BankerTypeRealmSend) + banker.SendCoins( + std.CurrentRealm().Addr(), + gameState.LastBuyer, + std.NewCoins(std.NewCoin("ugnot", gameState.Jackpot)), + ) + + emitGameEnded( + gameState.CurrentRound, + gameState.LastBuyer, + gameState.Jackpot, + ) + + // Mint NFT for the winner + if err := mintRoundWinnerNFT(gameState.LastBuyer, gameState.CurrentRound); err != nil { + panic(err.Error()) + } +} + +// GetGameState returns current game state +func GetGameState() (int64, int64, int64, std.Address, int64, int64, int64, bool, int64, int64) { + return gameState.StartBlock, + gameState.EndBlock, + gameState.LastKeyBlock, + gameState.LastBuyer, + gameState.Jackpot, + gameState.KeyPrice, + gameState.TotalKeys, + gameState.Ended, + gameState.NextPot, + gameState.CurrentRound +} + +// GetOwnerInfo returns the owner address and unclaimed fees +func GetOwnerInfo() (std.Address, int64) { + return Ownable.Owner(), gameState.OwnerFee +} + +// Helper to convert string (address or username) to address +func stringToAddress(input string) std.Address { + // Check if input is valid address + addr := std.Address(input) + if addr.IsValid() { + return addr + } + + // Not an address, try to find namespace + if user := users.GetUserByName(input); user != nil { + return user.Address + } + + return "" +} + +func isPlayerInGame(addr std.Address) bool { + _, exists := players.Get(addr.String()) + return exists +} + +// GetPlayerInfo returns a player's keys and dividends +func GetPlayerInfo(addrOrName string) (int64, int64) { + addr := stringToAddress(addrOrName) + + if addr == "" { + panic(ErrInvalidAddressOrName.Error()) + } + + if !isPlayerInGame(addr) { + panic(ErrPlayerNotInGame.Error()) + } + + info, _ := players.Get(addr.String()) + playerInfo := info.(PlayerInfo) + return playerInfo.Keys, playerInfo.Dividends +} + +// Render handles the rendering of game state +func Render(path string) string { + parts := strings.Split(path, "/") + c := len(parts) + + switch { + case path == "": + return RenderHome() + case c == 2 && parts[0] == "player": + if gameState.Ended { + return ufmt.Sprintf("🔴 Game has not started yet.\n\n Call [`StartGame()`](%s) to start a new round.\n\n", gameState.StartGameLink) + } + addr := stringToAddress(parts[1]) + if addr == "" || !isPlayerInGame(addr) { + return "Address not found in game. You need to buy keys first to view your stats.\n\n" + } + keys, dividends := GetPlayerInfo(parts[1]) + return RenderPlayer(addr, keys, dividends) + default: + return "404: Invalid path\n\n" + } +} diff --git a/examples/gno.land/r/stefann/fomo3d/fomo3d_test.gno b/examples/gno.land/r/stefann/fomo3d/fomo3d_test.gno new file mode 100644 index 00000000000..29f2a9b07a9 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/fomo3d_test.gno @@ -0,0 +1,294 @@ +package fomo3d + +import ( + "std" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/grc/grc721" + "gno.land/p/demo/ownable" + "gno.land/p/demo/testutils" + "gno.land/p/demo/urequire" +) + +// Reset game state +func setupTestGame(t *testing.T) { + gameState = GameState{ + StartBlock: 0, + EndBlock: 0, + LastKeyBlock: 0, + LastBuyer: "", + Jackpot: 0, + KeyPrice: MIN_KEY_PRICE, + TotalKeys: 0, + Ended: true, + CurrentRound: 0, + NextPot: 0, + OwnerFee: 0, + } + players = avl.NewTree() + Ownable = ownable.New() +} + +// Test ownership functionality +func TestOwnership(t *testing.T) { + owner := testutils.TestAddress("owner") + nonOwner := testutils.TestAddress("nonOwner") + + // Set up initial owner + std.TestSetOrigCaller(owner) + std.TestSetOrigPkgAddr(owner) + setupTestGame(t) + + // Transfer ownership to nonOwner first to test ownership functions + std.TestSetOrigCaller(owner) + urequire.NotPanics(t, func() { + Ownable.TransferOwnership(nonOwner) + }) + + // Test fee accumulation + StartGame() + payment := MIN_KEY_PRICE * 10 + std.TestSetOrigCaller(owner) + std.TestSetOrigSend(std.Coins{{"ugnot", payment}}, nil) + std.TestIssueCoins(owner, std.Coins{{"ugnot", payment}}) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", payment}}) + BuyKeys() + + // Verify fee accumulation + _, fees := GetOwnerInfo() + expectedFees := payment * OWNER_FEE_PERCENT / 100 + urequire.Equal(t, expectedFees, fees) + + // Test unauthorized fee claim (using old owner) + std.TestSetOrigCaller(owner) + urequire.PanicsWithMessage(t, "ownable: caller is not owner", ClaimOwnerFee) + + // Test authorized fee claim (using new owner) + std.TestSetOrigCaller(nonOwner) + initialBalance := std.GetBanker(std.BankerTypeRealmSend).GetCoins(nonOwner) + std.TestIssueCoins(std.CurrentRealm().Addr(), std.Coins{{"ugnot", expectedFees}}) + urequire.NotPanics(t, ClaimOwnerFee) + + // Verify fees were claimed + _, feesAfter := GetOwnerInfo() + urequire.Equal(t, int64(0), feesAfter) + + finalBalance := std.GetBanker(std.BankerTypeRealmSend).GetCoins(nonOwner) + urequire.Equal(t, initialBalance.AmountOf("ugnot")+expectedFees, finalBalance.AmountOf("ugnot")) +} + +// Test full game flow +func TestFullGameFlow(t *testing.T) { + setupTestGame(t) + + player1 := testutils.TestAddress("player1") + player2 := testutils.TestAddress("player2") + player3 := testutils.TestAddress("player3") + + // Test initial state + urequire.Equal(t, int64(0), gameState.CurrentRound) + urequire.Equal(t, MIN_KEY_PRICE, gameState.KeyPrice) + urequire.Equal(t, true, gameState.Ended) + + // Start game + urequire.NotPanics(t, StartGame) + urequire.Equal(t, false, gameState.Ended) + urequire.Equal(t, std.GetHeight(), gameState.StartBlock) + urequire.Equal(t, int64(1), gameState.CurrentRound) + + t.Run("buying keys", func(t *testing.T) { + // Test insufficient payment + std.TestSetOrigCaller(player1) + std.TestIssueCoins(player1, std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}) + urequire.PanicsWithMessage(t, ErrInsufficientPayment.Error(), BuyKeys) + + // Test successful key purchase + payment := MIN_KEY_PRICE * 3 + std.TestSetOrigSend(std.Coins{{"ugnot", payment}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", payment}}) + + currentBlock := std.GetHeight() + urequire.NotPanics(t, BuyKeys) + + // Verify time extension + _, endBlock, _, _, _, _, _, _, _, _ := GetGameState() + urequire.Equal(t, currentBlock+TIME_EXTENSION, endBlock) + + // Verify player state + keys, dividends := GetPlayerInfo(player1.String()) + + urequire.Equal(t, int64(3), keys) + urequire.Equal(t, int64(0), dividends) + urequire.Equal(t, player1, gameState.LastBuyer) + + // Verify game state + _, endBlock, _, buyer, pot, price, keys, isEnded, nextPot, round := GetGameState() + urequire.Equal(t, player1, buyer) + urequire.Equal(t, int64(3), keys) + urequire.Equal(t, false, isEnded) + + urequire.Equal(t, payment*JACKPOT_PERCENT/100, pot) + + // Verify owner fee + _, ownerFees := GetOwnerInfo() + urequire.Equal(t, payment*OWNER_FEE_PERCENT/100, ownerFees) + }) + + t.Run("dividend distribution and claiming", func(t *testing.T) { + // Player 2 buys keys + std.TestSetOrigCaller(player2) + payment := gameState.KeyPrice * 2 // Buy 2 keys using current keyPrice + std.TestSetOrigSend(std.Coins{{"ugnot", payment}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", payment}}) + urequire.NotPanics(t, BuyKeys) + + // Check player1 received dividends + keys1, dividends1 := GetPlayerInfo(player1.String()) + + urequire.Equal(t, int64(3), keys1) + expectedDividends := payment * DIVIDENDS_PERCENT / 100 * 3 / gameState.TotalKeys + urequire.Equal(t, expectedDividends, dividends1) + + // Test claiming dividends + { + // Player1 claims dividends + std.TestSetOrigCaller(player1) + initialBalance := std.GetBanker(std.BankerTypeRealmSend).GetCoins(player1) + urequire.NotPanics(t, ClaimDividends) + + // Verify dividends were claimed + _, dividendsAfter := GetPlayerInfo(player1.String()) + urequire.Equal(t, int64(0), dividendsAfter) + + lastBuyerBalance := std.GetBanker(std.BankerTypeRealmSend).GetCoins(player1) + urequire.Equal(t, initialBalance.AmountOf("ugnot")+expectedDividends, lastBuyerBalance.AmountOf("ugnot")) + } + }) + + t.Run("game ending", func(t *testing.T) { + // Try ending too early + urequire.PanicsWithMessage(t, ErrGameNotInProgress.Error(), EndGame) + + // Skip to end of current time window + currentEndBlock := gameState.EndBlock + std.TestSkipHeights(currentEndBlock - std.GetHeight() + 1) + + // End game successfully + urequire.NotPanics(t, EndGame) + urequire.Equal(t, true, gameState.Ended) + urequire.Equal(t, int64(1), gameState.CurrentRound) + + // Verify winner received jackpot + lastBuyerBalance := std.GetBanker(std.BankerTypeRealmSend).GetCoins(gameState.LastBuyer) + urequire.Equal(t, gameState.Jackpot, lastBuyerBalance.AmountOf("ugnot")) + + // Verify NFT was minted to winner + balance, err := BalanceOf(gameState.LastBuyer) + urequire.NoError(t, err) + urequire.Equal(t, uint64(1), balance) + + // Check NFT metadata + tokenID := grc721.TokenID("1") + metadata, err := TokenMetadata(tokenID) + + urequire.NoError(t, err) + urequire.Equal(t, "Fomo3D Winner - Round #1", metadata.Name) + }) + + // Test new round + t.Run("new round", func(t *testing.T) { + // Calculate expected next pot from previous round + payment1 := MIN_KEY_PRICE * 3 + // After buying 3 keys, price increased by 3% (1% per key) + secondKeyPrice := MIN_KEY_PRICE + (MIN_KEY_PRICE * 3 / 100) + payment2 := secondKeyPrice * 2 + expectedNextPot := (payment1 * NEXT_ROUND_POT / 100) + (payment2 * NEXT_ROUND_POT / 100) + + // Start new round + urequire.NotPanics(t, StartGame) + urequire.Equal(t, false, gameState.Ended) + urequire.Equal(t, int64(2), gameState.CurrentRound) + + start, end, last, buyer, pot, price, keys, isEnded, nextPot, round := GetGameState() + urequire.Equal(t, int64(2), round) + urequire.Equal(t, expectedNextPot, pot) + urequire.Equal(t, int64(0), nextPot) + }) +} + +// Test individual components +func TestStartGame(t *testing.T) { + setupTestGame(t) + + // Test starting first game + urequire.NotPanics(t, StartGame) + urequire.Equal(t, false, gameState.Ended) + urequire.Equal(t, std.GetHeight(), gameState.StartBlock) + + // Test cannot start while game in progress + urequire.PanicsWithMessage(t, ErrGameInProgress.Error(), StartGame) +} + +func TestBuyKeys(t *testing.T) { + setupTestGame(t) + StartGame() + + player := testutils.TestAddress("player") + std.TestSetOrigCaller(player) + + // Test invalid coin denomination + std.TestIssueCoins(player, std.Coins{{"invalid", MIN_KEY_PRICE}}) + std.TestSetOrigSend(std.Coins{{"invalid", MIN_KEY_PRICE}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"invalid", MIN_KEY_PRICE}}) + urequire.PanicsWithMessage(t, ErrInvalidPayment.Error(), BuyKeys) + + // Test multiple coin types + std.TestIssueCoins(player, std.Coins{{"ugnot", MIN_KEY_PRICE}, {"other", 100}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE}, {"other", 100}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE}, {"other", 100}}) + urequire.PanicsWithMessage(t, ErrInvalidPayment.Error(), BuyKeys) + + // Test insufficient payment + std.TestIssueCoins(player, std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}) + urequire.PanicsWithMessage(t, ErrInsufficientPayment.Error(), BuyKeys) + + // Test successful purchase + std.TestIssueCoins(player, std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}) + urequire.NotPanics(t, BuyKeys) +} + +func TestClaimDividends(t *testing.T) { + setupTestGame(t) + StartGame() + + player := testutils.TestAddress("player") + std.TestSetOrigCaller(player) + + // Test claiming with no dividends + urequire.PanicsWithMessage(t, ErrNoDividendsToClaim.Error(), ClaimDividends) + + // Setup player with dividends + std.TestIssueCoins(player, std.Coins{{"ugnot", MIN_KEY_PRICE}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE}}) + BuyKeys() + + // Have another player buy to generate dividends + player2 := testutils.TestAddress("player2") + std.TestSetOrigCaller(player2) + std.TestIssueCoins(player2, std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}) + BuyKeys() + + // Test successful claim + std.TestSetOrigCaller(player) + urequire.NotPanics(t, ClaimDividends) +} diff --git a/examples/gno.land/r/stefann/fomo3d/gno.mod b/examples/gno.land/r/stefann/fomo3d/gno.mod new file mode 100644 index 00000000000..1b4e630a285 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/gno.mod @@ -0,0 +1 @@ +module gno.land/r/stefann/fomo3d diff --git a/examples/gno.land/r/stefann/fomo3d/nft.gno b/examples/gno.land/r/stefann/fomo3d/nft.gno new file mode 100644 index 00000000000..adea2fee795 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/nft.gno @@ -0,0 +1,88 @@ +package fomo3d + +import ( + "std" + "strconv" + + "gno.land/p/demo/grc/grc721" +) + +var ( + fomo3dNFT = grc721.NewNFTWithMetadata("Fomo3D Winner", "FOMO") +) + +// Public getters + +func Name() string { + return fomo3dNFT.Name() +} + +func Symbol() string { + return fomo3dNFT.Symbol() +} + +func BalanceOf(owner std.Address) (uint64, error) { + return fomo3dNFT.BalanceOf(owner) +} + +func OwnerOf(tokenID grc721.TokenID) (std.Address, error) { + return fomo3dNFT.OwnerOf(tokenID) +} + +func TokenMetadata(tokenID grc721.TokenID) (grc721.Metadata, error) { + return fomo3dNFT.TokenMetadata(tokenID) +} + +// Transfer and approval methods + +func TransferFrom(from, to std.Address, tokenID grc721.TokenID) error { + return fomo3dNFT.TransferFrom(from, to, tokenID) +} + +func SafeTransferFrom(from, to std.Address, tokenID grc721.TokenID) error { + return fomo3dNFT.SafeTransferFrom(from, to, tokenID) +} + +func Approve(approved std.Address, tokenID grc721.TokenID) error { + return fomo3dNFT.Approve(approved, tokenID) +} + +func GetApproved(tokenID grc721.TokenID) (std.Address, error) { + return fomo3dNFT.GetApproved(tokenID) +} + +func SetApprovalForAll(operator std.Address, approved bool) error { + return fomo3dNFT.SetApprovalForAll(operator, approved) +} + +func IsApprovedForAll(owner, operator std.Address) bool { + return fomo3dNFT.IsApprovedForAll(owner, operator) +} + +// Mints a new NFT for the round winner +func mintRoundWinnerNFT(winner std.Address, roundNumber int64) error { + if winner == "" { + return ErrZeroAddress + } + + roundStr := strconv.FormatInt(roundNumber, 10) + tokenID := grc721.TokenID(roundStr) + + // Create metadata + metadata := grc721.Metadata{ + Name: "Fomo3D Winner - Round #" + roundStr, + Description: "Winner of Fomo3D round #" + roundStr, + Image: "https://ipfs.io/ipfs/bafybeidayyli6bpewkhgtwqpgubmo77kmgjn4r5zq2i7usoyadcmvynhhq", + ExternalURL: "https://gno.land/r/stefann/fomo3d:round/" + roundStr, // TODO: Add this render in main realm that shows details of specific round + Attributes: []grc721.Trait{}, + BackgroundColor: "2D2D2D", // Dark theme background + } + + if err := fomo3dNFT.Mint(winner, tokenID); err != nil { + return err + } + + fomo3dNFT.SetTokenMetadata(tokenID, metadata) + + return nil +} diff --git a/examples/gno.land/r/stefann/fomo3d/render.gno b/examples/gno.land/r/stefann/fomo3d/render.gno new file mode 100644 index 00000000000..ba0c7b8f147 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/render.gno @@ -0,0 +1,138 @@ +package fomo3d + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/demo/grc/grc721" + "gno.land/p/demo/ufmt" + + "gno.land/r/demo/users" +) + +// RenderHome renders the main game state +func RenderHome() string { + var builder strings.Builder + builder.WriteString("# FOMO3D - The Ultimate Game of Greed\n\n") + + // About section + builder.WriteString("## About the Game\n\n") + builder.WriteString("FOMO3D is a game that combines elements of lottery and investment mechanics. ") + builder.WriteString("Players purchase keys using GNOT tokens, where each key purchase:\n\n") + builder.WriteString("* Extends the game timer\n") + builder.WriteString("* Increases the key price by 1%\n") + builder.WriteString("* Makes you the potential winner of the jackpot\n") + builder.WriteString("* Distributes dividends to all key holders\n\n") + builder.WriteString("## How to Win\n\n") + builder.WriteString("* Be the last person to buy a key before the timer expires!\n\n") + builder.WriteString("**Rewards Distribution:**\n") + builder.WriteString("* 47% goes to the jackpot (for the winner)\n") + builder.WriteString("* 28% distributed as dividends to all key holders\n") + builder.WriteString("* 20% goes to next round's starting pot\n") + builder.WriteString("* 5% development fee for continuous improvement\n\n") + + // Play Game section + builder.WriteString("## How to Play\n\n") + builder.WriteString(ufmt.Sprintf("1. **Buy Keys** - Send GNOT to this realm with function [`BuyKeys()`](%s)\n", gameState.BuyKeysLink)) + builder.WriteString(ufmt.Sprintf("2. **Collect Dividends** - Call [`ClaimDividends()`](%s) to collect your earnings\n", gameState.ClaimDividendsLink)) + builder.WriteString("3. **Check Your Stats** - Append `:player/` followed by your address or namespace to the current URL to view your keys and dividends\n") + if gameState.Ended { + builder.WriteString(ufmt.Sprintf("4. **Start New Round** - Call [`StartGame()`](%s) to begin a new round\n", gameState.StartGameLink)) + } + builder.WriteString("\n") + + // Game Status section + builder.WriteString("## Game Status\n\n") + if gameState.StartBlock == 0 { + builder.WriteString("🔴 Game has not started yet.\n\n") + } else { + if gameState.Ended { + builder.WriteString("🔴 **Game Status:** Ended\n") + builder.WriteString(ufmt.Sprintf("🏆 **Winner:** %s\n\n", gameState.LastBuyer)) + } else { + builder.WriteString("🟢 **Game Status:** Active\n\n") + builder.WriteString(ufmt.Sprintf("🔄 **Round:** %d\n\n", gameState.CurrentRound)) + builder.WriteString(ufmt.Sprintf("⏱️ **Time Remaining:** %d blocks\n\n", gameState.EndBlock-std.GetHeight())) + } + builder.WriteString(ufmt.Sprintf("💰 **Jackpot:** %d ugnot\n\n", gameState.Jackpot)) + builder.WriteString(ufmt.Sprintf("🔑 **Key Price:** %d ugnot\n\n", gameState.KeyPrice)) + builder.WriteString(ufmt.Sprintf("📊 **Total Keys:** %d\n\n", gameState.TotalKeys)) + builder.WriteString(ufmt.Sprintf("👤 **Last Buyer:** %s\n\n", getDisplayName(gameState.LastBuyer))) + builder.WriteString(ufmt.Sprintf("🎮 **Next Round Pot:** %d ugnot\n\n", gameState.NextPot)) + } + + // Separator before less important sections + builder.WriteString("---\n\n") + + // Vote For Me section + builder.WriteString("### Vote For Us! 🗳️\n\n") + builder.WriteString("If you enjoy playing FOMO3D, please consider upvoting this game in the [Hall of Realms](https://gno.land/r/leon/hof)!\n\n") + builder.WriteString("Your support helps more players discover the game and grow our community! 🚀\n\n") + + // Report Bug section + builder.WriteString("### Report a Bug 🪲\n\n") + builder.WriteString("Something unusual happened? Help us improve the game by reporting bugs!\n") + builder.WriteString("[Visit our GitHub repository](https://github.com/gnolang/gno/issues)\n\n") + builder.WriteString("Please include:\n") + builder.WriteString("* Detailed description of what happened\n") + builder.WriteString("* Transaction hash (if applicable)\n") + builder.WriteString("* Your address\n") + builder.WriteString("* Current round number\n") + + return builder.String() +} + +// RenderPlayer renders specific player information +func RenderPlayer(addr std.Address, keys int64, dividends int64) string { + var builder strings.Builder + displayName := getDisplayName(addr) + builder.WriteString(ufmt.Sprintf("# Player Stats: %s\n\n", displayName)) + builder.WriteString("## Your Holdings\n\n") + builder.WriteString(ufmt.Sprintf("🔑 **Keys Owned:** %d\n\n", keys)) + builder.WriteString(ufmt.Sprintf("💰 **Unclaimed Dividends:** %d ugnot\n\n", dividends)) + + // Check if player has any NFTs + nftBalance, err := BalanceOf(addr) + if err == nil && nftBalance > 0 { + builder.WriteString("## Your Victory NFTs 🏆\n\n") + + // Iterate through all rounds up to current round to find player's NFTs + for i := int64(1); i <= gameState.CurrentRound; i++ { + tokenID := grc721.TokenID(strconv.FormatInt(i, 10)) + owner, err := OwnerOf(tokenID) + if err == nil && owner == addr { + metadata, err := TokenMetadata(tokenID) + if err == nil { + builder.WriteString(ufmt.Sprintf("### Round #%d Winner\n", i)) + builder.WriteString(ufmt.Sprintf("![NFT](%s)\n\n", metadata.Image)) + builder.WriteString("---\n\n") + } + } + } + } + + builder.WriteString("## Actions\n\n") + builder.WriteString(ufmt.Sprintf("* To buy more keys, send GNOT to this realm with [`BuyKeys()`](%s)\n", gameState.BuyKeysLink)) + if dividends > 0 { + builder.WriteString("* You have unclaimed dividends! Call `ClaimDividends()` to collect them\n") + } + + return builder.String() +} + +// Helper to get display name - just returns namespace if exists, otherwise address +func getDisplayName(addr std.Address) string { + if user := users.GetUserByAddress(addr); user != nil { + return user.Name + } + return addr.String() +} + +// UpdateFunctionLinks updates the links for game functions +func UpdateFunctionLinks(buyKeysLink string, claimDividendsLink string, startGameLink string) { + Ownable.AssertCallerIsOwner() + gameState.BuyKeysLink = buyKeysLink + gameState.ClaimDividendsLink = claimDividendsLink + gameState.StartGameLink = startGameLink +} diff --git a/gno.land/pkg/gnoweb/webclient_html.go b/gno.land/pkg/gnoweb/webclient_html.go index c04a7f9e457..72b1b3f8b06 100644 --- a/gno.land/pkg/gnoweb/webclient_html.go +++ b/gno.land/pkg/gnoweb/webclient_html.go @@ -49,6 +49,7 @@ func NewDefaultHTMLWebClientConfig(client *client.RPCClient) *HTMLWebClientConfi markdown.NewHighlighting( markdown.WithFormatOptions(chromaOptions...), ), + extension.Strikethrough, extension.Table, ), }