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(x/ecocredit): CreateProject ORM #823

Merged
merged 4 commits into from
Mar 4, 2022
Merged
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
86 changes: 86 additions & 0 deletions x/ecocredit/server/core/create_project.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package core

import (
"context"
"github.com/cosmos/cosmos-sdk/orm/types/ormerrors"
ecocreditv1beta1 "github.com/regen-network/regen-ledger/api/regen/ecocredit/v1beta1"
"github.com/regen-network/regen-ledger/types"
"github.com/regen-network/regen-ledger/x/ecocredit"
"github.com/regen-network/regen-ledger/x/ecocredit/v1beta1"
)

// CreateProject creates a new project for a specific credit class.
func (k Keeper) CreateProject(ctx context.Context, req *v1beta1.MsgCreateProject) (*v1beta1.MsgCreateProjectResponse, error) {
aleem1314 marked this conversation as resolved.
Show resolved Hide resolved
sdkCtx := types.UnwrapSDKContext(ctx)
classID := req.ClassId
classInfo, err := k.stateStore.ClassInfoStore().GetByName(ctx, classID)
if err != nil {
return nil, err
}

err = k.assertClassIssuer(ctx, classInfo.Id, req.Issuer)
if err != nil {
return nil, err
}

projectID := req.ProjectId
if projectID == "" {
exists := true
for ; exists; sdkCtx.GasMeter().ConsumeGas(gasCostPerIteration, "project id sequence") {
projectID, err = k.genProjectID(ctx, classInfo.Id, classInfo.Name)
if err != nil {
return nil, err
}
exists, err = k.stateStore.ProjectInfoStore().HasByClassIdName(ctx, classInfo.Id, projectID)
if err != nil {
return nil, err
}
}
}

if err = k.stateStore.ProjectInfoStore().Insert(ctx, &ecocreditv1beta1.ProjectInfo{
Name: projectID,
ClassId: classInfo.Id,
ProjectLocation: req.ProjectLocation,
Metadata: req.Metadata,
}); err != nil {
return nil, err
}

if err := sdkCtx.EventManager().EmitTypedEvent(&v1beta1.EventCreateProject{
ClassId: classID,
ProjectId: projectID,
Issuer: req.Issuer,
ProjectLocation: req.ProjectLocation,
}); err != nil {
return nil, err
}

return &v1beta1.MsgCreateProjectResponse{
ProjectId: projectID,
}, nil
}

// genProjectID generates a projectID when no projectID was given for CreateProject.
// The ID is generated by concatenating the classID and a sequence number.
func (k Keeper) genProjectID(ctx context.Context, classRowID uint64, classID string) (string, error) {
var nextID uint64
projectSeqNo, err := k.stateStore.ProjectSequenceStore().Get(ctx, classRowID)
switch err {
case ormerrors.NotFound:
nextID = 1
case nil:
nextID = projectSeqNo.NextProjectId
default:
return "", err
}

if err = k.stateStore.ProjectSequenceStore().Save(ctx, &ecocreditv1beta1.ProjectSequence{
ClassId: classRowID,
NextProjectId: nextID + 1,
}); err != nil {
return "", err
}

return ecocredit.FormatProjectID(classID, nextID), nil
}
101 changes: 101 additions & 0 deletions x/ecocredit/server/core/create_project_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package core

import (
"context"
"github.com/cosmos/cosmos-sdk/orm/types/ormerrors"
"github.com/cosmos/cosmos-sdk/types"
ecocreditv1beta1 "github.com/regen-network/regen-ledger/api/regen/ecocredit/v1beta1"
"github.com/regen-network/regen-ledger/x/ecocredit/v1beta1"
"gotest.tools/v3/assert"
"testing"
)

func TestCreateProject_ValidProjectState(t *testing.T) {
t.Parallel()
s := setupBase(t)
makeClass(t, s.ctx, s.stateStore, s.addr)
res, err := s.k.CreateProject(s.ctx, &v1beta1.MsgCreateProject{
Issuer: s.addr.String(),
ClassId: "C01",
Metadata: nil,
ProjectLocation: "US-NY",
ProjectId: "FOO",
})
assert.NilError(t, err)
assert.Equal(t, res.ProjectId, "FOO")

project, err := s.stateStore.ProjectInfoStore().GetByName(s.ctx, "FOO")
assert.NilError(t, err)
assert.Equal(t, project.ProjectLocation, "US-NY")
}

func TestCreateProject_GeneratedProjectID(t *testing.T) {
t.Parallel()
s := setupBase(t)
makeClass(t, s.ctx, s.stateStore, s.addr)
res, err := s.k.CreateProject(s.ctx, &v1beta1.MsgCreateProject{
Issuer: s.addr.String(),
ClassId: "C01",
Metadata: nil,
ProjectLocation: "US-NY",
ProjectId: "",
})
assert.NilError(t, err)
assert.Equal(t, res.ProjectId, "C0101", "got project id: %s", res.ProjectId)

res, err = s.k.CreateProject(s.ctx, &v1beta1.MsgCreateProject{
Issuer: s.addr.String(),
ClassId: "C01",
Metadata: nil,
ProjectLocation: "US-NY",
ProjectId: "",
})
assert.NilError(t, err)
assert.Equal(t, res.ProjectId, "C0102", "got project id: %s", res.ProjectId)
}

func TestCreateProject_BadClassID(t *testing.T) {
t.Parallel()
s := setupBase(t)
_, err := s.k.CreateProject(s.ctx, &v1beta1.MsgCreateProject{
Issuer: s.addr.String(),
ClassId: "NOPE",
ProjectLocation: "US-NY",
ProjectId: "",
})
assert.ErrorContains(t, err, "not found")
}

func TestCreateProject_NoDuplicates(t *testing.T) {
t.Parallel()
s := setupBase(t)
makeClass(t, s.ctx, s.stateStore, s.addr)
_, err := s.k.CreateProject(s.ctx, &v1beta1.MsgCreateProject{
Issuer: s.addr.String(),
ClassId: "C01",
ProjectLocation: "US-NY",
ProjectId: "FOO",
})
assert.NilError(t, err)

_, err = s.k.CreateProject(s.ctx, &v1beta1.MsgCreateProject{
Issuer: s.addr.String(),
ClassId: "C01",
ProjectLocation: "US-NY",
ProjectId: "FOO",
})
assert.ErrorContains(t, err, ormerrors.UniqueKeyViolation.Error())
}

func makeClass(t *testing.T, ctx context.Context, ss ecocreditv1beta1.StateStore, addr types.AccAddress) {
assert.NilError(t, ss.ClassInfoStore().Insert(ctx, &ecocreditv1beta1.ClassInfo{
Name: "C01",
Admin: addr,
Metadata: nil,
CreditType: "C",
}))
assert.NilError(t, ss.ClassIssuerStore().Insert(ctx, &ecocreditv1beta1.ClassIssuer{
ClassId: 1,
Issuer: addr,
}))
}
4 changes: 4 additions & 0 deletions x/ecocredit/server/core/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import (
"github.com/regen-network/regen-ledger/x/ecocredit"
)

// TODO: Revisit this once we have proper gas fee framework.
// Tracking issues https://github.com/cosmos/cosmos-sdk/issues/9054, https://github.com/cosmos/cosmos-sdk/discussions/9072
const gasCostPerIteration = uint64(10)

type Keeper struct {
stateStore ecocreditv1beta1.StateStore
bankKeeper ecocredit.BankKeeper
Expand Down
21 changes: 21 additions & 0 deletions x/ecocredit/server/core/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package core

import (
"context"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

// assertClassIssuer makes sure that the issuer is part of issuers of given classID.
// Returns ErrUnauthorized otherwise.
func (k Keeper) assertClassIssuer(goCtx context.Context, classID uint64, issuer string) error {
addr, _ := sdk.AccAddressFromBech32(issuer)
found, err := k.stateStore.ClassIssuerStore().Has(goCtx, classID, addr)
if err != nil {
return err
}
if !found {
return sdkerrors.ErrUnauthorized.Wrapf("%s is not an issuer for the class", issuer)
}
return nil
}