Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added migration & ownable #166

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion realm/chess.gno
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"time"

"gno.land/p/demo/avl"
"gno.land/p/demo/ufmt"
)

// realm state
Expand All @@ -20,6 +19,10 @@ var (

// Value must be sorted by game ID, descending
user2Games avl.Tree // std.Address -> []*Game

owner std.Address // admin of realm, can initiate migration
migrated bool // true if migrated, all exported realm functions panic
migratedTo string // new realm deployment path in case of migration
)

// Game represents a chess game.
Expand All @@ -40,13 +43,21 @@ type Game struct {
Time *TimeControl `json:"time"`
}

func init() {
owner = std.GetOrigCaller()
migrated = false
migratedTo = ""
}

func (g Game) json() string {
s, err := g.MarshalJSON()
checkErr(err)
return string(s)
}

func (g Game) MarshalJSON() (_ []byte, err error) {
checkMigrated()

var b bytes.Buffer
b.WriteByte('{')

Expand Down Expand Up @@ -98,6 +109,8 @@ func (g Game) MarshalJSON() (_ []byte, err error) {
}

func (p Position) MarshalJSON() ([]byte, error) {
checkMigrated()

var b bytes.Buffer
b.WriteByte('{')

Expand Down Expand Up @@ -137,6 +150,8 @@ var winnerString = [...]string{
}

func (w Winner) MarshalJSON() ([]byte, error) {
checkMigrated()

if n := int(w); n < len(winnerString) {
return []byte(`"` + winnerString[n] + `"`), nil
}
Expand Down Expand Up @@ -189,6 +204,8 @@ var gameStatesSnake = [...]string{
}

func (g GameState) MarshalJSON() ([]byte, error) {
checkMigrated()

if int(g) >= len(gameStatesSnake) {
return nil, errors.New("invalid game state")
}
Expand All @@ -197,6 +214,8 @@ func (g GameState) MarshalJSON() ([]byte, error) {

// IsFinished returns whether the game is in a finished state.
func (g GameState) IsFinished() bool {
checkMigrated()

return g != GameStateOpen
}

Expand All @@ -213,6 +232,8 @@ func (g GameState) IsFinished() bool {
// parallel games OR by introducing a "request" system, so that a game is not
// immediately considered "open" when calling NewGame.
func xNewGame(opponentRaw string, seconds, increment int) string {
checkMigrated()

std.AssertOriginCall()

if seconds >= 0 && increment < 0 {
Expand Down Expand Up @@ -331,6 +352,8 @@ func determineColor(games []*Game, caller, opponent std.Address) (isBlack bool)

// GetGame returns a game, knowing its ID.
func GetGame(id string) string {
checkMigrated()

return getGame(id, false).json()
}

Expand All @@ -352,6 +375,8 @@ func getGame(id string, wantOpen bool) *Game {
// must be specified.
// Castling is specified by indicating the king's movement.
func MakeMove(gameID, from, to string, promote Piece) string {
checkMigrated()

std.AssertOriginCall()

g := getGame(gameID, true)
Expand Down Expand Up @@ -480,6 +505,8 @@ func (g *Game) claimTimeout() error {
// ClaimTimeout should be called when the caller believes the game has resulted
// in a timeout.
func ClaimTimeout(gameID string) string {
checkMigrated()

g := getGame(gameID, true)

err := g.claimTimeout()
Expand All @@ -489,6 +516,8 @@ func ClaimTimeout(gameID string) string {
}

func Abort(gameID string) string {
checkMigrated()

std.AssertOriginCall()

g := getGame(gameID, true)
Expand All @@ -510,6 +539,8 @@ func Abort(gameID string) string {
}

func Resign(gameID string) string {
checkMigrated()

std.AssertOriginCall()

g := getGame(gameID, true)
Expand Down Expand Up @@ -541,6 +572,8 @@ func resign(g *Game) error {
// DrawOffer creates a draw offer in the current game, if one doesn't already
// exist.
func DrawOffer(gameID string) string {
checkMigrated()

std.AssertOriginCall()

g := getGame(gameID, true)
Expand All @@ -559,6 +592,8 @@ func DrawOffer(gameID string) string {

// DrawRefuse refuse a draw offer in the given game.
func DrawRefuse(gameID string) string {
checkMigrated()

std.AssertOriginCall()

g := getGame(gameID, true)
Expand All @@ -583,6 +618,8 @@ func DrawRefuse(gameID string) string {
// - Insufficient material (§9.4)
// Note: stalemate happens as a consequence of a Move, and thus is handled in that function.
func Draw(gameID string) string {
checkMigrated()

std.AssertOriginCall()

g := getGame(gameID, true)
Expand Down
29 changes: 29 additions & 0 deletions realm/migrate.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package chess

func checkMigrated() {
if migrated {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if !migrated {return}
...

Copy link
Contributor Author

@leohhhn leohhhn Sep 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you intend to handle the situation where the realm has not migrated yet? If that is the case, since migrated is a bool, the only stateful change that will happen is if it is true. Why check otherwise?

msg := "realm has migrated to " + migratedTo
panic(msg)
}
}

func Migrate(newRealmPath string) string {
if newRealmPath == "" {
panic("new realm path cannot be empty")
}

if !isOwner() {
panic("not owner")
}

migrated = true
migratedTo = newRealmPath
return "successfully migrated to " + newRealmPath
}

func GetMigration() string {
if migratedTo == "" {
return "realm has not migrated"
}
return migratedTo
}
46 changes: 46 additions & 0 deletions realm/migrate_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package chess

import (
"std"
"testing"
)

func TestMigrate(t *testing.T) {
std.TestSetOrigCaller("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm")
if migrated != false || migratedTo != "" {
t.Fatal("Bad init")
}

Migrate("gno.land/r/demo/chessV2")

if migrated != true || migratedTo != "gno.land/r/demo/chessV2" {
t.Fatal("Migration failed")
}
}

func TestMigrate_EmptyPath(t *testing.T) {
std.TestSetOrigCaller("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm")

defer func() {
if r := recover(); r == nil {
t.Error("Expected Migrate to panic with an empty path, but it didn't")
}
}()

Migrate("")
}

func TestMigrate_BlockedFunction(t *testing.T) {
std.TestSetOrigCaller("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm")

Migrate("example/path")

defer func() {
if r := recover(); r == nil {
t.Error("Expected ClaimTimeout to panic with migrated realm, but did not")
}
}()

// example blocked function
ClaimTimeout("exampleGameID")
}
30 changes: 30 additions & 0 deletions realm/ownable.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package chess

import "std"

// TransferOwnership
// Warning: Use carefully - if owner is misconfigured, realm will be bricked
func TransferOwnership(newOwner std.Address) string {
if newOwner == "" {
panic("new owner cannot be empty")
}

if !isOwner() {
panic("not owner")
}

owner = newOwner
return "new owner is " + string(owner)
}

func isOwner() bool {
caller := std.GetOrigCaller()
if caller != GetOwner() {
return false
}
return true
}

func GetOwner() std.Address {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this exported?

Copy link
Contributor Author

@leohhhn leohhhn Sep 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that it can be called by gnokey, as to check who is the owner of realm

return owner
}
47 changes: 47 additions & 0 deletions realm/ownable_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package chess

import (
"std"
"testing"
)

func TestTransferOwnership_NotOwner(t *testing.T) {
std.TestSetOrigCaller("g185rxzrsc0j69amfsl255k2fxtlnt3cw8f3eu6h") // random address

defer func() {
if r := recover(); r == nil {
t.Error("Expected TransferOwnership to panic with not owner, but did not")
}
}()

TransferOwnership("newOwner")
}

func TestTransferOwnership_EmptyNewOwner(t *testing.T) {
std.TestSetOrigCaller("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm")

defer func() {
if r := recover(); r == nil {
t.Error("Expected TransferOwnership to panic with empty new owner, but did not")
}
}()

TransferOwnership("")
}

func TestTransferOwnership(t *testing.T) {
// Note:
// it seems that this address is the result of std.GetOrigCaller() in the init function when testing
// will open up an issue to address this
std.TestSetOrigCaller("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm")

if GetOwner() != "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" {
t.Fatal("original owner not correct")
}

TransferOwnership("g185rxzrsc0j69amfsl255k2fxtlnt3cw8f3eu6h")

if GetOwner() != "g185rxzrsc0j69amfsl255k2fxtlnt3cw8f3eu6h" {
t.Fatal("new owner not set correctly")
}
}
30 changes: 30 additions & 0 deletions realm/state.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package chess

import (
"gno.land/p/demo/avl"
)

type RealmState struct {
// chess.gno
gameStore avl.Tree // string (game ID) -> *Game
gameIDCounter uint64
user2Games avl.Tree // std.Address -> []*Game

// discovery.gno
playerStore avl.Tree // std.Address -> *Player
leaderboard [CategoryMax]leaderboardType

// glicko2.gno
playerRatings [CategoryMax][]*PlayerRating
}

func RetrieveRealmState() *RealmState {
return &RealmState{
gameStore: gameStore,
gameIDCounter: gameIDCounter,
user2Games: user2Games,
playerStore: playerStore,
leaderboard: leaderboard,
playerRatings: playerRatings,
}
}
Loading