diff --git a/examples/gno.land/r/gh/README.md b/examples/gno.land/r/gh/README.md new file mode 100644 index 00000000000..43bff14cbbd --- /dev/null +++ b/examples/gno.land/r/gh/README.md @@ -0,0 +1,64 @@ +# GitHub Realm + +**Disclaimer**: This realm is not designed to automatically score or rank pull +requests. Its primary purpose is to provide factual, reliable data from GitHub +to the on-chain environment. Any interpretation or scoring of this data should +be handled by other systems or contracts. + +Welcome to the GitHub realm. This suite of contracts is designed to bridge the +gap between GitHub's vast repositories of data and our on-chain environment. The +overarching aim is to translate select GitHub metrics and interactions onto the +blockchain, providing a seamless interface between the two ecosystems. + +## Purpose of the Package + +The main goals of this package are as follows: + +1. **Oracle-Filled Data**: The package will primarily be populated by an oracle + that translates and mirrors select GitHub data onto the chain. This ensures a + reliable and consistent flow of data between GitHub and our on-chain + ecosystem. + +2. **User Account Linkage**: Users can establish a connection between their + GitHub accounts and their on-chain identities, strengthening the bond between + off-chain activities and on-chain representations. + +3. **Helper Functions for Gno Contract Integration**: A series of helper + functions will be available to convert GitHub objects into Gno objects. This + will enable other contracts to seamlessly utilize GitHub data within their + logic and operations. + +## Key Features + +### On-Chain Representation of GitHub Metrics + +GitHub is a treasure trove of valuable metrics. This package will mirror +essential GitHub data on-chain, like issues, PR statuses, and more. The goal is +to provide an on-chain management system that reflects GitHub's activities, +ensuring the two platforms remain interlinked. + +### Bidirectional Data Flow + +While our primary focus is to mirror GitHub activities on-chain, the reverse +process is also integral. On-chain activities should be recognizable on GitHub, +allowing for a holistic data flow between the two systems. + +### User Account Linkage + +To bolster user interaction and maintain a reliable data flow, users can link +their GitHub and on-chain accounts. This bi-directional linkage offers users a +cohesive experience, and broadens our spectrum of interactivity. + +### Helper Functions + +Developers can tap into a range of helper functions, which can transform GitHub +data into Gno-compatible objects. This aids in integrating GitHub's vast +datasets into other on-chain contracts and logics. + +## Conclusion + +The GitHub package for Gno is a pioneering step towards integrating off-chain +data sources with on-chain functionalities. As we evolve this package, we remain +committed to maintaining the integrity of data, ensuring fairness, and enhancing +the user experience. Feedback, contributions, and suggestions are always +welcome! diff --git a/examples/gno.land/r/gh/accounts.gno b/examples/gno.land/r/gh/accounts.gno new file mode 100644 index 00000000000..f274017a2d7 --- /dev/null +++ b/examples/gno.land/r/gh/accounts.gno @@ -0,0 +1,52 @@ +package gh + +import "errors" + +// Account represents a GitHub user account or organization. +type Account struct { + id string + name string + kind string +} + +func (a Account) ID() string { return a.id } +func (a Account) Name() string { return a.name } +func (a Account) Kind() string { return a.kind } +func (a Account) URL() string { return "https://github.com/" + a.id } +func (a Account) IsUser() bool { return a.kind == UserAccount } +func (a Account) IsOrg() bool { return a.kind == OrgAccount } + +// TODO: func (a Account) RepoByID() Repo ... + +func (a Account) Validate() error { + if a.id == "" { + return errors.New("empty id") + } + if a.kind == "" { + return errors.New("empty kind") + } + if a.name == "" { + return errors.New("empty name") + } + // TODO: validate + return nil +} + +func (a Account) String() string { + // XXX: better idea? + return a.URL() +} + +const ( + UserAccount string = "user" + OrgAccount string = "org" +) + +func AccountByID(id string) *Account { + res, ok := accounts.Get(id) + if !ok { + return nil + } + + return res.(*Account) +} diff --git a/examples/gno.land/r/gh/accounts_test.gno b/examples/gno.land/r/gh/accounts_test.gno new file mode 100644 index 00000000000..53cf8114b7d --- /dev/null +++ b/examples/gno.land/r/gh/accounts_test.gno @@ -0,0 +1,50 @@ +package gh + +import ( + "testing" +) + +// Test for the Account struct functions. +func TestAccountFunctions(t *testing.T) { + account := Account{ + id: "user123", + name: "John Doe", + kind: "user", + } + + t.Run("Test Account ID", func(t *testing.T) { + if account.ID() != "user123" { + t.Fatalf("Expected ID to be user123, got %s", account.ID()) + } + }) + + t.Run("Test Account Name", func(t *testing.T) { + if account.Name() != "John Doe" { + t.Fatalf("Expected Name to be John Doe, got %s", account.Name()) + } + }) + + t.Run("Test Account Kind", func(t *testing.T) { + if account.Kind() != "user" { + t.Fatalf("Expected Kind to be user, got %s", account.Kind()) + } + }) + + t.Run("Test Account URL", func(t *testing.T) { + if account.URL() != "https://github.com/user123" { + t.Fatalf("Expected URL to be https://github.com/user123, got %s", account.URL()) + } + }) + + t.Run("Test Account IsUser", func(t *testing.T) { + if !account.IsUser() { + t.Fatal("Expected IsUser to be true") + } + }) + + t.Run("Test Account IsOrg", func(t *testing.T) { + if account.IsOrg() { + t.Fatal("Expected IsOrg to be false") + } + }) +} diff --git a/examples/gno.land/r/gh/gno.mod b/examples/gno.land/r/gh/gno.mod new file mode 100644 index 00000000000..4988dfe2e22 --- /dev/null +++ b/examples/gno.land/r/gh/gno.mod @@ -0,0 +1 @@ +module gno.land/r/gh diff --git a/examples/gno.land/r/gh/issues_prs.gno b/examples/gno.land/r/gh/issues_prs.gno new file mode 100644 index 00000000000..d47ea43cd97 --- /dev/null +++ b/examples/gno.land/r/gh/issues_prs.gno @@ -0,0 +1,18 @@ +package gh + +/* +// IssueOrPR represents a GitHub issue or pull request +type IssueOrPR struct { + ID int + Title string + Body string + Author *Account + Repo *Repo + Type string +} + +const ( + Issue = "issue" + PR = "pr" +) +*/ diff --git a/examples/gno.land/r/gh/oracle.gno b/examples/gno.land/r/gh/oracle.gno new file mode 100644 index 00000000000..45668fc6a18 --- /dev/null +++ b/examples/gno.land/r/gh/oracle.gno @@ -0,0 +1,102 @@ +package gh + +import ( + "std" + "strings" + "time" + + "gno.land/p/demo/avl" +) + +var ( + accounts avl.Tree // uri -> Account + repos avl.Tree // uri -> Repo + issueOrPRs avl.Tree // uri -> IssueOrPR + lastUpdateTime time.Time // used by the bot to only upload the diff + oracleAddr std.Address = "g1eunnckcl6r8ncwj0lrpxu9g5062xcvwxqlrf29" + adminAddr std.Address = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" // @manfred +) + +func OracleLastUpdated() time.Time { return lastUpdateTime } + +func OracleUpsertAccount(id, name, kind string) { + assertIsOracle() + lastUpdateTime = time.Now() + + // get or create + account := &Account{} + res, ok := accounts.Get(id) + if ok { + account = res.(*Account) + } else { + account.id = id + } + + // update fields + account.name = name + account.kind = kind + + if err := account.Validate(); err != nil { + panic(err) + } + + // save + accounts.Set(id, account) +} + +func OracleUpsertRepo(id string, isPrivate, isFork bool) { + assertIsOracle() + lastUpdateTime = time.Now() + + // get or create + repo := &Repo{} + res, ok := repos.Get(id) + if ok { + repo = res.(*Repo) + } else { + repo.id = id + } + + parts := strings.Split(id, "/") + if len(parts) != 2 { + panic("invalid id") + } + ownerID := parts[0] + name := parts[1] + + // update fields + repo.name = name + repo.isPrivate = isPrivate + repo.isFork = isFork + repo.owner = AccountByID(ownerID) + + if err := repo.Validate(); err != nil { + panic(err) + } + + // save + repos.Set(id, repo) +} + +func AdminSetOracleAddr(new std.Address) { + assertIsAdmin() + oracleAddr = new +} + +// XXX: remove once it will be easy to query private variables' state. +func AdminGetOracleAddr() std.Address { return oracleAddr } + +func assertIsAdmin() { + if std.GetOrigCaller() != adminAddr { + panic("restricted area") + } +} + +func assertIsOracle() { + if std.GetOrigCaller() != oracleAddr { + panic("restricted area") + } +} + +// TODO: could be a great fit for a vector-based/state machine approach, mostly for optimizations +// func OracleApplyVectors(vectors ...) diff --git a/examples/gno.land/r/gh/public.gno b/examples/gno.land/r/gh/public.gno new file mode 100644 index 00000000000..53aafef3cdf --- /dev/null +++ b/examples/gno.land/r/gh/public.gno @@ -0,0 +1,8 @@ +package gh + +func LinkAccount(account string, signature string) { + // TODO: verify signature + // TODO: upsert AccountLink + // TODO: generate challenge to be signed and published on gh + panic("not implemented") +} diff --git a/examples/gno.land/r/gh/render.gno b/examples/gno.land/r/gh/render.gno new file mode 100644 index 00000000000..137f753d3c4 --- /dev/null +++ b/examples/gno.land/r/gh/render.gno @@ -0,0 +1,5 @@ +package gh + +func Render(path string) string { + panic("not implemented") +} diff --git a/examples/gno.land/r/gh/repos.gno b/examples/gno.land/r/gh/repos.gno new file mode 100644 index 00000000000..18b6b2f5cfd --- /dev/null +++ b/examples/gno.land/r/gh/repos.gno @@ -0,0 +1,46 @@ +package gh + +import "errors" + +// Repo represents a GitHub repository. +type Repo struct { + id string + owner *Account + name string + isPrivate bool + isFork bool +} + +func (r Repo) ID() string { return r.id } +func (r Repo) Name() string { return r.name } +func (r Repo) Owner() *Account { return r.owner } +func (r Repo) IsPrivate() bool { return r.isPrivate } +func (r Repo) IsFork() bool { return r.isFork } +func (r Repo) URL() string { return r.owner.URL() + "/" + r.name } + +func (r Repo) String() string { + // XXX: better idea? + return r.URL() +} + +func (r Repo) Validate() error { + if r.id == "" { + return errors.New("id is empty") + } + if r.name == "" { + return errors.New("name is empty") + } + if r.owner == nil { + return errors.New("owner is nil") + } + return nil +} + +func RepoByID(id string) *Repo { + res, ok := repos.Get(id) + if !ok { + return nil + } + + return res.(*Repo) +} diff --git a/examples/gno.land/r/gh/repos_test.gno b/examples/gno.land/r/gh/repos_test.gno new file mode 100644 index 00000000000..374b31704f0 --- /dev/null +++ b/examples/gno.land/r/gh/repos_test.gno @@ -0,0 +1,41 @@ +package gh + +import "testing" + +// Test for the Repo struct functions. +func TestRepoFunctions(t *testing.T) { + account := &Account{ + id: "org123", + name: "Sample Org", + kind: "org", + } + repo := Repo{ + id: "org123/sample-repo", + owner: account, + name: "sample-repo", + } + + t.Run("Test Repo ID", func(t *testing.T) { + if repo.ID() != "org123/sample-repo" { + t.Fatalf("Expected ID to be org123/sample-repo, got %s", repo.ID()) + } + }) + + t.Run("Test Repo Name", func(t *testing.T) { + if repo.Name() != "sample-repo" { + t.Fatalf("Expected Name to be sample-repo, got %s", repo.Name()) + } + }) + + t.Run("Test Repo Owner", func(t *testing.T) { + if repo.Owner().ID() != "org123" { + t.Fatalf("Expected Owner ID to be org123, got %s", repo.Owner().ID()) + } + }) + + t.Run("Test Repo URL", func(t *testing.T) { + if repo.URL() != "https://github.com/org123/sample-repo" { + t.Fatalf("Expected URL to be https://github.com/org123/sample-repo, got %s", repo.URL()) + } + }) +} diff --git a/examples/gno.land/r/gh/z1_filetest.gno b/examples/gno.land/r/gh/z1_filetest.gno new file mode 100644 index 00000000000..a941288e375 --- /dev/null +++ b/examples/gno.land/r/gh/z1_filetest.gno @@ -0,0 +1,34 @@ +package main + +import ( + "std" + + "gno.land/p/demo/testutils" + "gno.land/r/gh" +) + +var ( + adminAddr = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") + oracleAddr = std.Address("g1eunnckcl6r8ncwj0lrpxu9g5062xcvwxqlrf29") + aliceAddr = testutils.TestAddress("alice") +) + +func main() { + std.TestSetOrigCaller(oracleAddr) + gh.OracleUpsertAccount("moul", "Manfred", "user") + gh.OracleUpsertAccount("gnolang", "Gno Ecosystem", "org") + gh.OracleUpsertRepo("gnolang/gno", false, false) + moul := gh.AccountByID("moul") + gnolang := gh.AccountByID("gnolang") + gnolangGno := gh.RepoByID("gnolang/gno") + println("gnoland: ", gnolang.String()) + println("gnolangGno: ", gnolangGno.String()) + println("moul: ", moul.String()) + println("Done.") +} + +// Output: +// gnoland: https://github.com/gnolang +// gnolangGno: https://github.com/gnolang/gno +// moul: https://github.com/moul +// Done. diff --git a/examples/gno.land/r/gh/z2_filetest.gno b/examples/gno.land/r/gh/z2_filetest.gno new file mode 100644 index 00000000000..21a55a96b76 --- /dev/null +++ b/examples/gno.land/r/gh/z2_filetest.gno @@ -0,0 +1,138 @@ +// z2_filetest_extended.gno + +package main + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/r/gh" +) + +const ( + adminAddr = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") + oracleAddr = std.Address("g1eunnckcl6r8ncwj0lrpxu9g5062xcvwxqlrf29") + aliceAddr = testutils.TestAddress("alice") + bobAddr = testutils.TestAddress("bob") +) + +func main() { + t := &testing.T{} + + testAdminSetOracleAddr(t) + testOracleUpsertAccount(t) + testOracleUpsertRepo(t) + testRepoByID(t) + testAccountByID(t) + testRepoFields(t) + testAlicePublicFunctions(t) + + if t.Failed() { + panic("Some tests failed.") + } else { + println("All tests passed!") + } +} + +func testAdminSetOracleAddr(t *testing.T) { + std.TestSetOrigCaller(adminAddr) + newOracleAddr := std.Address("g1neworcl1234567890abcdef1234567890abcdef") + gh.AdminSetOracleAddr(newOracleAddr) + + expected := newOracleAddr + result := gh.AdminGetOracleAddr() + if result != expected { + t.Errorf("Expected oracle address to be set to %s, got %s", expected, result) + } + + // Resetting back to original oracle for subsequent tests + gh.AdminSetOracleAddr(oracleAddr) +} + +func testOracleUpsertAccount(t *testing.T) { + std.TestSetOrigCaller(oracleAddr) + gh.OracleUpsertAccount("moul", "Manfred", "user") + gh.OracleUpsertAccount("gnolang", "Gno Ecosystem", "org") + + moul := gh.AccountByID("moul") + gnolang := gh.AccountByID("gnolang") + + { + expected := "Manfred" + result := moul.Name() + if result != expected { + t.Errorf("Expected moul's name to be %s, got %s", expected, result) + } + } + + { + expected := "org" + result := gnolang.Kind() + if result != expected { + t.Errorf("Expected gnolang's kind to be %s, got %s", expected, result) + } + } +} + +func testOracleUpsertRepo(t *testing.T) { + std.TestSetOrigCaller(oracleAddr) + gh.OracleUpsertRepo("gnolang/gno", false, false) + + repo := gh.RepoByID("gnolang/gno") + expected := "gno" + result := repo.Name() + if result != expected { + t.Errorf("Expected repo name to be %s, got %s", expected, result) + } +} + +func testRepoByID(t *testing.T) { + repo := gh.RepoByID("gnolang/gno") + if repo == nil { + t.Errorf("Expected to find a repo with ID gnolang/gno, got nil") + } +} + +func testAccountByID(t *testing.T) { + acc := gh.AccountByID("moul") + if acc == nil { + t.Errorf("Expected to find an account with ID moul, got nil") + } +} + +func testRepoFields(t *testing.T) { + repo := gh.RepoByID("gnolang/gno") + if repo.IsFork() { + t.Errorf("Expected repo to not be a fork") + } + + if repo.IsPrivate() { + t.Errorf("Expected repo to not be private") + } + + expected := "https://github.com/gnolang/gno" + result := repo.URL() + if result != expected { + t.Errorf("Expected repo URL to be %s, got %s", expected, result) + } +} + +func testAlicePublicFunctions(t *testing.T) { + std.TestSetOrigCaller(aliceAddr) + + // Test AccountByID + acc := gh.AccountByID("moul") + if acc == nil || acc.Name() != "Manfred" { + t.Errorf("Alice could not fetch the account 'moul'.") + } + + // Test RepoByID + repo := gh.RepoByID("gnolang/gno") + if repo == nil || repo.Name() != "gno" { + t.Errorf("Alice could not fetch the repo 'gnolang/gno'.") + } +} + +// Output: +// All tests passed!