govdao.gno

package impl

import (
	"chain"
	"chain/runtime"
	"errors"

	"gno.land/p/nt/ufmt/v0"
	"gno.land/r/gov/dao"
	"gno.land/r/gov/dao/v3/memberstore"
)

var ErrMemberNotFound = errors.New("member not found")

type GovDAO struct {
	pss    ProposalsStatuses
	render *render
}

func NewGovDAO() *GovDAO {
	pss := NewProposalsStatuses()
	d := &GovDAO{
		pss: pss,
	}

	d.render = NewRender(d)

	// There was no realm, from main(), so it succeeded, And
	// when returning, there was no finalization.  We don't
	// finalize anyways because there wasn't a realm boundary.
	// XXX make filetest main package a realm.
	//
	// filetest.init() ->
	//   v3/init.Init() ->
	//     NewGovDAO() ->
	//       returns an unsaved DAO NOTE NO REALM!
	//     dao.UpdateImpl =>
	//       saves dao under
	//
	// r/gov/dao.CrossPropposal() ->
	//   proposals.SetProposal(),
	//     that proposal lives in r/gov/dao.
	// r/gov/dao.ExecuteProposal() ->
	//   g.PreExecuteProposal() ->
	//     XXX g.test = 1 fails, owned by gov/dao.
	//
	//
	func(cur realm) {
		// TODO: replace with future attach()
		_govdao = d
	}(cross)

	return d
}

// Setting this to a global variable forces attaching the GovDAO struct to this
// realm. TODO replace with future `attach()`.
var _govdao *GovDAO

func (g *GovDAO) PreCreateProposal(r dao.ProposalRequest) (address, error) {
	if !g.isValidCall() {
		return "", errors.New(ufmt.Sprintf("proposal creation must be done directly by a user or through the r/gov/dao proxy. current realm: %v; previous realm: %v",
			runtime.CurrentRealm(), runtime.PreviousRealm()))
	}

	// Verify that the one creating the proposal is a member.
	caller := runtime.OriginCaller()
	mem, _ := getMembers(cross).GetMember(caller)
	if mem == nil {
		return caller, errors.New("only members can create new proposals")
	}

	return caller, nil
}

func (g *GovDAO) PostCreateProposal(r dao.ProposalRequest, pid dao.ProposalID) {
	// Tiers Allowed to Vote
	tatv := []string{memberstore.T1, memberstore.T2, memberstore.T3}
	switch v := r.Filter().(type) {
	case FilterByTier:
		// only members from T1 are allowed to vote when adding new members to T1
		if v.Tier == memberstore.T1 {
			tatv = []string{memberstore.T1}
		}
		// only members from T1 and T2 are allowed to vote when adding new members to T2
		if v.Tier == memberstore.T2 {
			tatv = []string{memberstore.T1, memberstore.T2}
		}
	}
	g.pss.Set(pid.String(), newProposalStatus(tatv))
}

func (g *GovDAO) VoteOnProposal(r dao.VoteRequest) error {
	if !g.isValidCall() {
		return errors.New("proposal voting must be done directly by a user")
	}

	caller := runtime.OriginCaller()
	mem, tie := getMembers(cross).GetMember(caller)
	if mem == nil {
		return ErrMemberNotFound
	}

	status := g.pss.GetStatus(r.ProposalID)
	if status == nil {
		return errors.New("proposal not found")
	}

	if status.Denied || status.Accepted {
		return errors.New(ufmt.Sprintf("proposal closed. Accepted: %v", status.Accepted))
	}

	if !status.IsAllowed(tie) {
		return errors.New("member on specified tier is not allowed to vote on this proposal")
	}

	mVoted, _ := status.AllVotes.GetMember(caller)
	if mVoted != nil {
		return errors.New("already voted on proposal")
	}

	switch r.Option {
	case dao.YesVote:
		status.AllVotes.SetMember(tie, caller, mem)
		status.YesVotes.SetMember(tie, caller, mem)
	case dao.NoVote:
		status.AllVotes.SetMember(tie, caller, mem)
		status.NoVotes.SetMember(tie, caller, mem)
	default:
		return errors.New("voting can only be YES or NO")
	}

	return nil
}

func (g *GovDAO) PreGetProposal(pid dao.ProposalID) error {
	return nil
}

func (g *GovDAO) PostGetProposal(pid dao.ProposalID, p *dao.Proposal) error {
	return nil
}

func (g *GovDAO) PreExecuteProposal(pid dao.ProposalID) (bool, error) {
	if !g.isValidCall() {
		return false, errors.New("proposal execution must be done directly by a user")
	}
	status := g.pss.GetStatus(pid)
	if status.Denied || status.Accepted {
		return false, errors.New(ufmt.Sprintf("proposal already executed. Accepted: %v", status.Accepted))
	}

	if status.YesPercent() >= law.Supermajority {
		status.Accepted = true
		return true, nil
	}

	if status.NoPercent() >= law.Supermajority {
		status.Denied = true
		return false, nil
	}

	return false, errors.New(ufmt.Sprintf("proposal didn't reach supermajority yet: %v", law.Supermajority))
}

func (g *GovDAO) Render(pkgPath string, path string) string {
	return g.render.Render(pkgPath, path)
}

func (g *GovDAO) isValidCall() bool {
	// We need to verify two cases:
	// 1: r/gov/dao (proxy) functions called directly by an user
	// 2: r/gov/dao/v3/impl methods called directly by an user

	// case 1
	if runtime.CurrentRealm().PkgPath() == "gno.land/r/gov/dao" {
		// called directly by an user through MsgCall
		if runtime.PreviousRealm().IsUser() {
			return true
		}
		isMsgRun := chain.PackageAddress(runtime.PreviousRealm().PkgPath()) == runtime.OriginCaller()
		// called directly by an user through MsgRun
		if isMsgRun {
			return true
		}
	}

	// case 2
	if runtime.CurrentRealm().IsUser() {
		return true
	}

	return false
}