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: add methods for updating a credit class #539

Merged
merged 9 commits into from
Sep 23, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ require (
github.com/tendermint/tendermint v0.34.12
github.com/tendermint/tm-db v0.6.4
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83 // indirect
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af // indirect
)

replace google.golang.org/grpc => google.golang.org/grpc v1.33.2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1207,8 +1207,8 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83 h1:3V2dxSZpz4zozWWUq36vUxXEKnSYitEH2LdsAx+RUmg=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af h1:aLMMXFYqw01RA6XJim5uaN+afqNNjc9P8HPAbnpnc5s=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/grpc v1.33.2 h1:EQyQC3sa8M+p6Ulc8yy9SWSS2GVwyRc83gAbG8lrl4o=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
Expand Down
59 changes: 58 additions & 1 deletion proto/regen/ecocredit/v1alpha1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ service Msg {
// deducts them from the tradable supply, effectively cancelling their
// issuance on Regen Ledger
rpc Cancel(MsgCancel) returns (MsgCancelResponse);

// UpdateClassAdmin updates the credit class admin
rpc UpdateClassAdmin(MsgUpdateClassAdmin) returns (MsgUpdateClassAdminResponse);

// UpdateClassIssuers updates the credit class issuer list
rpc UpdateClassIssuers(MsgUpdateClassIssuers) returns (MsgUpdateClassIssuersResponse);

// UpdateClassMetadata updates the credit class meta data
technicallyty marked this conversation as resolved.
Show resolved Hide resolved
rpc UpdateClassMetadata(MsgUpdateClassMetadata) returns (MsgUpdateClassMetadataResponse);
}

// MsgCreateClass is the Msg/CreateClass request type.
Expand Down Expand Up @@ -217,4 +226,52 @@ message MsgCancel {
}

// MsgCancelResponse is the Msg/Cancel response type.
message MsgCancelResponse {}
message MsgCancelResponse {}

// MsgUpdateClassAdmin is the Msg/UpdateClassAdmin request type.
message MsgUpdateClassAdmin {
technicallyty marked this conversation as resolved.
Show resolved Hide resolved

// admin is the address of the account that is the admin of the credit class.
string admin = 1;

// class_id is the unique ID of the credit class.
string class_id = 2;

// new_admin is the address of the new admin of the credit class.
string new_admin = 3;
}

// MsgUpdateClassAdminResponse is the MsgUpdateClassAdmin response type.
message MsgUpdateClassAdminResponse {}

// MsgUpdateClassIssuers is the Msg/UpdateClassIssuers request type.
message MsgUpdateClassIssuers {

// admin is the address of the account that is the admin of the credit class.
string admin = 1;

// class_id is the unique ID of the credit class.
string class_id = 2;

// issuers are the updated account addresses of the approved issuers.
repeated string issuers = 3;
}

// MsgUpdateClassIssuersResponse is the MsgUpdateClassIssuers response type.
message MsgUpdateClassIssuersResponse {}

// MsgUpdateClassMetadata is the Msg/UpdateClassMetadata request type.
message MsgUpdateClassMetadata {

// admin is the address of the account that is the admin of the credit class.
string admin = 1;

// class_id is the unique ID of the credit class.
string class_id = 2;

// metadata is the updated arbitrary metadata to be attached to the credit class.
bytes metadata = 3;
}

// MsgUpdateClassMetadataResponse is the MsgUpdateClassMetadata response type.
message MsgUpdateClassMetadataResponse {}
101 changes: 97 additions & 4 deletions x/ecocredit/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import (
)

var (
_, _, _, _, _ sdk.Msg = &MsgCreateClass{}, &MsgCreateBatch{}, &MsgSend{},
&MsgRetire{}, &MsgCancel{}
_, _, _, _, _ legacytx.LegacyMsg = &MsgCreateClass{}, &MsgCreateBatch{}, &MsgSend{},
&MsgRetire{}, &MsgCancel{}
_, _, _, _, _, _, _, _ sdk.Msg = &MsgCreateClass{}, &MsgCreateBatch{}, &MsgSend{},
&MsgRetire{}, &MsgCancel{}, &MsgUpdateClassAdmin{}, &MsgUpdateClassIssuers{}, &MsgUpdateClassMetadata{}
_, _, _, _, _, _, _, _ legacytx.LegacyMsg = &MsgCreateClass{}, &MsgCreateBatch{}, &MsgSend{},
&MsgRetire{}, &MsgCancel{}, &MsgUpdateClassAdmin{}, &MsgUpdateClassIssuers{}, &MsgUpdateClassMetadata{}
)

// Route Implements LegacyMsg.
Expand Down Expand Up @@ -254,3 +254,96 @@ func (m *MsgCancel) GetSigners() []sdk.AccAddress {
addr, _ := sdk.AccAddressFromBech32(m.Holder)
return []sdk.AccAddress{addr}
}

func (m MsgUpdateClassAdmin) Route() string { return sdk.MsgTypeURL(&m) }

func (m MsgUpdateClassAdmin) Type() string { return sdk.MsgTypeURL(&m) }

func (m MsgUpdateClassAdmin) GetSignBytes() []byte {
return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m))
}

func (m *MsgUpdateClassAdmin) ValidateBasic() error {
if m.Admin == m.NewAdmin {
return sdkerrors.ErrInvalidAddress.Wrap("new admin should be a different address from the signer")
}

if _, err := sdk.AccAddressFromBech32(m.Admin); err != nil {
return sdkerrors.ErrInvalidAddress
}

if _, err := sdk.AccAddressFromBech32(m.NewAdmin); err != nil {
return sdkerrors.ErrInvalidAddress
}

if err := ValidateClassID(m.ClassId); err != nil {
return err
}

return nil
}

func (m *MsgUpdateClassAdmin) GetSigners() []sdk.AccAddress {
addr, _ := sdk.AccAddressFromBech32(m.Admin)
return []sdk.AccAddress{addr}
}

func (m MsgUpdateClassIssuers) Route() string { return sdk.MsgTypeURL(&m) }

func (m MsgUpdateClassIssuers) Type() string { return sdk.MsgTypeURL(&m) }

func (m MsgUpdateClassIssuers) GetSignBytes() []byte {
return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m))
}

func (m *MsgUpdateClassIssuers) ValidateBasic() error {
if _, err := sdk.AccAddressFromBech32(m.Admin); err != nil {
return sdkerrors.ErrInvalidAddress
}

if err := ValidateClassID(m.ClassId); err != nil {
return err
}

if len(m.Issuers) == 0 {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

should we allow issuers to be empty in this method, so someone can wipe the issuer list?

Copy link
Member

Choose a reason for hiding this comment

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

Good question. We require issuers when creating a credit class, so I would lean towards requiring issuers when updating a credit class. I could however see wiping issuers as a way of disabling the credit class but the admin could also assign themselves as the only issuer to accomplish.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

okay, leaving as is

return sdkerrors.ErrInvalidRequest.Wrap("issuers cannot be empty")
}

for _, addr := range m.Issuers {
if _, err := sdk.AccAddressFromBech32(addr); err != nil {
return sdkerrors.ErrInvalidAddress
}
}

return nil
}

func (m *MsgUpdateClassIssuers) GetSigners() []sdk.AccAddress {
addr, _ := sdk.AccAddressFromBech32(m.Admin)
return []sdk.AccAddress{addr}
}

func (m MsgUpdateClassMetadata) Route() string { return sdk.MsgTypeURL(&m) }

func (m MsgUpdateClassMetadata) Type() string { return sdk.MsgTypeURL(&m) }

func (m MsgUpdateClassMetadata) GetSignBytes() []byte {
return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m))
}

func (m *MsgUpdateClassMetadata) ValidateBasic() error {
technicallyty marked this conversation as resolved.
Show resolved Hide resolved
if _, err := sdk.AccAddressFromBech32(m.Admin); err != nil {
return sdkerrors.ErrInvalidAddress
}

if err := ValidateClassID(m.ClassId); err != nil {
return err
}

return nil
}

func (m *MsgUpdateClassMetadata) GetSigners() []sdk.AccAddress {
addr, _ := sdk.AccAddressFromBech32(m.Admin)
return []sdk.AccAddress{addr}
}
121 changes: 121 additions & 0 deletions x/ecocredit/msgs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -751,3 +751,124 @@ func TestMsgCancel(t *testing.T) {
})
}
}

func TestMsgUpdateClassAdmin(t *testing.T) {
_, _, admin := testdata.KeyTestPubAddr()
_, _, newAdmin := testdata.KeyTestPubAddr()

tests := map[string]struct {
src MsgUpdateClassAdmin
expErr bool
}{
"valid": {
src: MsgUpdateClassAdmin{Admin: admin.String(), NewAdmin: newAdmin.String(), ClassId: "C01"},
expErr: false,
},
"invalid: same address": {
src: MsgUpdateClassAdmin{Admin: admin.String(), NewAdmin: admin.String(), ClassId: "C01"},
expErr: true,
},
"invalid: bad ClassID": {
src: MsgUpdateClassAdmin{Admin: admin.String(), NewAdmin: newAdmin.String(), ClassId: "asl;dfjkdjk???fgs;dfljgk"},
expErr: true,
},
"invalid: bad admin addr": {
src: MsgUpdateClassAdmin{Admin: "?!@%)(87", NewAdmin: newAdmin.String(), ClassId: "C02"},
expErr: true,
},
"invalid: bad NewAdmin addr": {
src: MsgUpdateClassAdmin{Admin: admin.String(), NewAdmin: "?!?@%?@$#6", ClassId: "C02"},
expErr: true,
},
}

for msg, test := range tests {
t.Run(msg, func(t *testing.T) {
err := test.src.ValidateBasic()
if test.expErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}

func TestMsgUpdateClassIssuers(t *testing.T) {
_, _, a1 := testdata.KeyTestPubAddr()
_, _, a2 := testdata.KeyTestPubAddr()

tests := map[string]struct {
src MsgUpdateClassIssuers
expErr bool
}{
"valid": {
src: MsgUpdateClassIssuers{Admin: a2.String(), ClassId: "C01", Issuers: []string{a1.String()}},
expErr: false,
},
"invalid: no issuers": {
src: MsgUpdateClassIssuers{Admin: a2.String(), ClassId: "C01", Issuers: []string{}},
expErr: true,
},
"invalid: no class ID": {
src: MsgUpdateClassIssuers{Admin: a2.String(), ClassId: "", Issuers: []string{a1.String()}},
expErr: true,
},
"invalid: bad admin address": {
src: MsgUpdateClassIssuers{Admin: "//????.!", ClassId: "C01", Issuers: []string{a1.String()}},
expErr: true,
},
"invalid: bad class ID": {
src: MsgUpdateClassIssuers{Admin: a1.String(), ClassId: "s.1%?#%", Issuers: []string{a1.String()}},
expErr: true,
},
}

for msg, test := range tests {
t.Run(msg, func(t *testing.T) {
err := test.src.ValidateBasic()
if test.expErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}

func TestMsgUpdateClassMetadata(t *testing.T) {
_, _, a1 := testdata.KeyTestPubAddr()

tests := map[string]struct {
src MsgUpdateClassMetadata
expErr bool
}{
"valid": {
src: MsgUpdateClassMetadata{Admin: a1.String(), ClassId: "C01", Metadata: []byte("hello world")},
expErr: false,
},
"invalid: bad admin address": {
src: MsgUpdateClassMetadata{Admin: "???a!#)(%", ClassId: "C01", Metadata: []byte("hello world")},
expErr: true,
},
"invalid: bad class ID": {
src: MsgUpdateClassMetadata{Admin: a1.String(), ClassId: "6012949", Metadata: []byte("hello world")},
expErr: true,
},
"invalid: no class ID": {
src: MsgUpdateClassMetadata{Admin: a1.String()},
expErr: true,
},
}

for msg, test := range tests {
t.Run(msg, func(t *testing.T) {
err := test.src.ValidateBasic()
if test.expErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
51 changes: 51 additions & 0 deletions x/ecocredit/server/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,57 @@ func (s serverImpl) Cancel(goCtx context.Context, req *ecocredit.MsgCancel) (*ec
return &ecocredit.MsgCancelResponse{}, nil
}

func (s serverImpl) UpdateClassAdmin(goCtx context.Context, req *ecocredit.MsgUpdateClassAdmin) (*ecocredit.MsgUpdateClassAdminResponse, error) {
ctx := types.UnwrapSDKContext(goCtx)
cInfo, err := s.getClassInfo(ctx, req.ClassId)
if err != nil {
return nil, err
}

if cInfo.Admin != req.Admin {
return nil, sdkerrors.ErrUnauthorized.Wrapf("you are not the administrator of this class")
}

cInfo.Admin = req.NewAdmin
err = s.classInfoTable.Update(ctx, cInfo)

return &ecocredit.MsgUpdateClassAdminResponse{}, err
}

func (s serverImpl) UpdateClassIssuers(goCtx context.Context, req *ecocredit.MsgUpdateClassIssuers) (*ecocredit.MsgUpdateClassIssuersResponse, error) {
ctx := types.UnwrapSDKContext(goCtx)
cInfo, err := s.getClassInfo(ctx, req.ClassId)
if err != nil {
return nil, err
}

if cInfo.Admin != req.Admin {
return nil, sdkerrors.ErrUnauthorized.Wrapf("you are not the administrator of this class")
}

cInfo.Issuers = req.Issuers
err = s.classInfoTable.Update(ctx, cInfo)

return &ecocredit.MsgUpdateClassIssuersResponse{}, err
}

func (s serverImpl) UpdateClassMetadata(goCtx context.Context, req *ecocredit.MsgUpdateClassMetadata) (*ecocredit.MsgUpdateClassMetadataResponse, error) {
ctx := types.UnwrapSDKContext(goCtx)
cInfo, err := s.getClassInfo(ctx, req.ClassId)
if err != nil {
return nil, err
}

if cInfo.Admin != req.Admin {
return nil, sdkerrors.ErrUnauthorized.Wrapf("you are not the administrator of this class")
}

cInfo.Metadata = req.Metadata
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this should probably cost some gas as metadata can be variable length - doesnt look like we have a set hardcapped length for metadata length. we should also charge some gas based on the length itself

Copy link
Member

Choose a reason for hiding this comment

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

We have a max length of 256 following #540. We should make sure the metadata does not exceed this max length by adding a check in validate basic.

err = s.classInfoTable.Update(ctx, cInfo)

return &ecocredit.MsgUpdateClassMetadataResponse{}, err
}

// nextBatchInClass gets the sequence number for the next batch in the credit
// class and updates the class info with the new batch number
func (s serverImpl) nextBatchInClass(ctx types.Context, classInfo *ecocredit.ClassInfo) (uint64, error) {
Expand Down
Loading