From c76362d4721ef4c44ad2e602fedcb1ac0e33c6c7 Mon Sep 17 00:00:00 2001 From: Omar Sy Date: Sun, 13 Oct 2024 16:40:51 +0200 Subject: [PATCH] feat: draft add notable contribution --- gno/r/notablecontribution/gno.mod | 9 ++ gno/r/notablecontribution/public.gno | 186 ++++++++++++++++++++++ gno/r/notablecontribution/public_test.gno | 66 ++++++++ gno/r/notablecontribution/task.gno | 28 ++++ 4 files changed, 289 insertions(+) create mode 100644 gno/r/notablecontribution/gno.mod create mode 100644 gno/r/notablecontribution/public.gno create mode 100644 gno/r/notablecontribution/public_test.gno create mode 100644 gno/r/notablecontribution/task.gno diff --git a/gno/r/notablecontribution/gno.mod b/gno/r/notablecontribution/gno.mod new file mode 100644 index 0000000000..905d90f287 --- /dev/null +++ b/gno/r/notablecontribution/gno.mod @@ -0,0 +1,9 @@ +module gno.land/r/teritori/notablecontribution + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/gnorkle/feeds/static v0.0.0-latest + gno.land/p/demo/gnorkle/gnorkle v0.0.0-latest + gno.land/p/demo/gnorkle/message v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest +) diff --git a/gno/r/notablecontribution/public.gno b/gno/r/notablecontribution/public.gno new file mode 100644 index 0000000000..36dd26f428 --- /dev/null +++ b/gno/r/notablecontribution/public.gno @@ -0,0 +1,186 @@ +package notablecontribution + +import ( + "std" + "errors" + "gno.land/p/demo/avl" + "gno.land/p/demo/gnorkle/gnorkle" + "gno.land/p/demo/gnorkle/message" + "gno.land/p/demo/gnorkle/feeds/static" +) + +type UserType string +type NotableContributionType string + +type User struct { + ID string + Type UserType +} + +func (u *User) JSON() (string) { + return `{"ID":"` + u.ID + `","Type":"` + string(u.Type) + `"}` +} + +func (u *User) String() string { + return u.ID + ":" + string(u.Type) +} + +type NotableContribution struct { + ID string + Type NotableContributionType + Description string + URL string + Date string +} + +func (n *NotableContribution) JSON() (string) { + return `{"ID":"` + string(n.ID) + `", "Type":"` + string(n.Type) + `","Description":"` + n.Description + `","URL":"` + n.URL + `","Date":"` + n.Date + `"}` +} + +type NotableContributions []NotableContribution + +func (n *NotableContributions) JSON() string { + var result string + for _, nc := range *n { + result += nc.JSON() + "," + } + return "[" + result[:len(result)-1] + "]" +} + +const ( + // User types + UserTypeUser UserType = "address" + UserGithub UserType = "github" + + // Notable contribution types + NotableContributionTypeCommit NotableContributionType = "commit" + NotableContributionTypeIssue NotableContributionType = "issue" + NotableContributionTypePR NotableContributionType = "pr" + NotableContributionTypeReview NotableContributionType = "review" + NotableContributionTypeComment NotableContributionType = "comment" + + verifiedResult string = "OK" +) + +var ( + postHandler postGnorkleMessageHandler + ownerAddress = std.GetOrigCaller() + userContributions *avl.Tree + oracle *gnorkle.Instance +) + +func init() { + oracle = gnorkle.NewInstance() + oracle.AddToWhitelist("", []string{string(ownerAddress)}) + userContributions = avl.NewTree() +} + +type postGnorkleMessageHandler struct{} + +// Handle implements the gnorkle.MessageHandler interface. +func (h postGnorkleMessageHandler) Handle(i *gnorkle.Instance, funcType message.FuncType, feed gnorkle.Feed) error { + if funcType != message.FuncTypeIngest { + return nil + } + + result, _, consumable := feed.Value() + if !consumable { + return nil + } + + defer oracle.RemoveFeed(feed.ID()) + + // Couldn't verify; nothing to do. + if result.String != verifiedResult { + return nil + } + + feedTasks := feed.Tasks() + if len(feedTasks) != 1 { + return errors.New("expected feed to have exactly one task") + } + + task, ok := feedTasks[0].(*verificationTask) + if !ok { + return errors.New("expected task to be of type *verificationTask") + } + + contributions := NotableContributions{} + res, ok := userContributions.Get(task.user.String()) + if ok { + contributions = res.(NotableContributions) + } + + contributions = append(contributions, task.notablecontribution) + userContributions.Set(task.user.String(), contributions) + + return nil +} + +func RequestNotableContribution(userID string, userType UserType, ntID string, ntType NotableContributionType, description, url, date string) { + user := User{ID: userID, Type: userType} + nt := NotableContribution{ + ID: ntID, + Type: ntType, Description: description, URL: url, Date: date} + + vt := &verificationTask{ + user: user, + notablecontribution: nt, + } + if err := oracle.AddFeeds( + static.NewSingleValueFeed( + vt.ID(), + "string", + vt, + ), + ); err != nil { + panic(err) + } +} + +func GnorkleEntrypoint(message string) string { + result, err := oracle.HandleMessage(message, postHandler) + if err != nil { + panic(err) + } + + return result +} + +// SetOwner transfers ownership of the contract to the given address. +func SetOwner(owner std.Address) { + if ownerAddress != std.GetOrigCaller() { + panic("only the owner can set a new owner") + } + + ownerAddress = owner + + // In the context of this contract, the owner is the only one that can + // add new feeds to the oracle. + oracle.ClearWhitelist("") + oracle.AddToWhitelist("", []string{string(ownerAddress)}) +} + +func GetUserContributions(userID, userType string) NotableContributions { + contributions, ok := userContributions.Get(userID + ":" + userType) + if !ok { + return NotableContributions{} + } + return contributions.(NotableContributions) +} + +func Render(path string) string { + result := `` + userContributions.Iterate("", "", func(user string, ctInf interface{}) bool { + contributions := ctInf.(NotableContributions) + result += `"`+ user + `":` + contributions.JSON() + `,` + return true + }) + + length := len(result) + if length == 0 { + return `{}` + } + + return `{` +result[:len(result)-1] + "}" +} diff --git a/gno/r/notablecontribution/public_test.gno b/gno/r/notablecontribution/public_test.gno new file mode 100644 index 0000000000..f4127bb2ba --- /dev/null +++ b/gno/r/notablecontribution/public_test.gno @@ -0,0 +1,66 @@ +package notablecontribution +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" +) + +func TestNotableContributionLifecycle(t *testing.T) { + defaultAddress := std.GetOrigCaller() + user1Address := std.Address(testutils.TestAddress("user 1")) + + result := GnorkleEntrypoint("request") + if result != "[]" { + t.Fatalf("expected empty request result, got %s", result) + } + + RequestNotableContribution("omarsy", UserGithub, "200032", NotableContributionTypeCommit, "", "https://github.com/gnolang/gno/commit/200032", "13/10/2024 15:51:23") + + var errMsg string + func() { + defer func() { + if r := recover(); r != nil { + errMsg = r.(error).Error() + } + }() + RequestNotableContribution("omarsy", UserGithub, "200032", NotableContributionTypeCommit, "", "https://github.com/gnolang/gno/commit/200032", "13/10/2024 15:51:23") + }() + if errMsg != "feed already exists" { + t.Fatalf("expected feed already exists, got %s", errMsg) + } + + result = GnorkleEntrypoint("request") + expResult := `[{"id":"omarsy:github:200032:commit","type":"0","value_type":"string","tasks":[{"user":{"ID":"omarsy","Type":"github"},"notablecontribution":{"ID":"200032", "Type":"commit","Description":"","URL":"https://github.com/gnolang/gno/commit/200032","Date":"13/10/2024 15:51:23"}}]}]` + if result != expResult { + t.Fatalf("expected request result %s, got %s", expResult, result) + } + + // Try to trigger feed ingestion from the non-authorized user. + std.TestSetOrigCaller(user1Address) + func() { + defer func() { + if r := recover(); r != nil { + errMsg = r.(error).Error() + } + }() + GnorkleEntrypoint("ingest,omarsy:github:200032:commit,OK") + }() + if errMsg != "caller not whitelisted" { + t.Fatalf("expected caller not whitelisted, got %s", errMsg) + } + std.TestSetOrigCaller(defaultAddress) + + GnorkleEntrypoint("ingest,omarsy:github:200032:commit,OK") + + result = GnorkleEntrypoint("request") + if result != `[]` { + t.Fatalf("expected empty request result, got %s", result) + } + + result = Render("") + expectedResult := `{"omarsy:github":[{"ID":"200032", "Type":"commit","Description":"","URL":"https://github.com/gnolang/gno/commit/200032","Date":"13/10/2024 15:51:23"}]}` + if result != expectedResult { + t.Fatalf("expected render result %s, got %s", expectedResult, result) + } +} diff --git a/gno/r/notablecontribution/task.gno b/gno/r/notablecontribution/task.gno new file mode 100644 index 0000000000..270ed5c0fd --- /dev/null +++ b/gno/r/notablecontribution/task.gno @@ -0,0 +1,28 @@ +package notablecontribution + +import ( + "bufio" + "bytes" +) + +type verificationTask struct { + user User + notablecontribution NotableContribution +} + +// MarshalJSON marshals the task contents to JSON. +func (t *verificationTask) MarshalJSON() ([]byte, error) { + buf := new(bytes.Buffer) + w := bufio.NewWriter(buf) + + w.Write( + []byte(`{"user":` + t.user.JSON() + `,"notablecontribution":` + t.notablecontribution.JSON() + `}`), + ) + + w.Flush() + return buf.Bytes(), nil +} + +func (t *verificationTask) ID() string { + return t.user.ID + ":" + string(t.user.Type) + ":" + t.notablecontribution.ID + ":" + string(t.notablecontribution.Type) +}