From ee2da4cf623fb6d6ff7d3748580ba9ab1646e140 Mon Sep 17 00:00:00 2001 From: technicallyty <48813565+tytech3@users.noreply.github.com> Date: Thu, 3 Mar 2022 10:12:11 -0800 Subject: [PATCH 1/3] feat(x/ecocredit): ORM --- x/ecocredit/server/core/create_project.go | 99 +++++++++++++++++ .../server/core/create_project_test.go | 101 ++++++++++++++++++ x/ecocredit/server/core/keeper.go | 4 + 3 files changed, 204 insertions(+) create mode 100644 x/ecocredit/server/core/create_project.go create mode 100644 x/ecocredit/server/core/create_project_test.go diff --git a/x/ecocredit/server/core/create_project.go b/x/ecocredit/server/core/create_project.go new file mode 100644 index 0000000000..322a60d9a0 --- /dev/null +++ b/x/ecocredit/server/core/create_project.go @@ -0,0 +1,99 @@ +package core + +import ( + "context" + "github.com/cosmos/cosmos-sdk/orm/types/ormerrors" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + 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" +) + +func (k Keeper) CreateProject(ctx context.Context, req *v1beta1.MsgCreateProject) (*v1beta1.MsgCreateProjectResponse, error) { + 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 +} + +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 +} + +// 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 +} diff --git a/x/ecocredit/server/core/create_project_test.go b/x/ecocredit/server/core/create_project_test.go new file mode 100644 index 0000000000..2a3ec33333 --- /dev/null +++ b/x/ecocredit/server/core/create_project_test.go @@ -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, + })) +} diff --git a/x/ecocredit/server/core/keeper.go b/x/ecocredit/server/core/keeper.go index c491023017..dc0ee850f1 100644 --- a/x/ecocredit/server/core/keeper.go +++ b/x/ecocredit/server/core/keeper.go @@ -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 From 3e1a5a41bd620d4f2d6731e9cc6320782e6ab460 Mon Sep 17 00:00:00 2001 From: technicallyty <48813565+tytech3@users.noreply.github.com> Date: Thu, 3 Mar 2022 11:06:40 -0800 Subject: [PATCH 2/3] chore: move general funcs to utils --- x/ecocredit/server/core/create_project.go | 16 ---------------- x/ecocredit/server/core/utils.go | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 x/ecocredit/server/core/utils.go diff --git a/x/ecocredit/server/core/create_project.go b/x/ecocredit/server/core/create_project.go index 322a60d9a0..bf780b33fd 100644 --- a/x/ecocredit/server/core/create_project.go +++ b/x/ecocredit/server/core/create_project.go @@ -3,8 +3,6 @@ package core import ( "context" "github.com/cosmos/cosmos-sdk/orm/types/ormerrors" - sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 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" @@ -83,17 +81,3 @@ func (k Keeper) genProjectID(ctx context.Context, classRowID uint64, classID str return ecocredit.FormatProjectID(classID, nextID), nil } - -// 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 -} diff --git a/x/ecocredit/server/core/utils.go b/x/ecocredit/server/core/utils.go new file mode 100644 index 0000000000..377ecdd00d --- /dev/null +++ b/x/ecocredit/server/core/utils.go @@ -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 +} From 4bd0de8ec305510b98fadad28627ccf842f020e3 Mon Sep 17 00:00:00 2001 From: technicallyty <48813565+tytech3@users.noreply.github.com> Date: Thu, 3 Mar 2022 13:13:20 -0800 Subject: [PATCH 3/3] chore: comments --- x/ecocredit/server/core/create_project.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x/ecocredit/server/core/create_project.go b/x/ecocredit/server/core/create_project.go index bf780b33fd..a6052d4c22 100644 --- a/x/ecocredit/server/core/create_project.go +++ b/x/ecocredit/server/core/create_project.go @@ -9,6 +9,7 @@ import ( "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) { sdkCtx := types.UnwrapSDKContext(ctx) classID := req.ClassId @@ -60,6 +61,8 @@ func (k Keeper) CreateProject(ctx context.Context, req *v1beta1.MsgCreateProject }, 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)