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(ecocredit/core): dynamic batch minting #1059

Merged
merged 17 commits into from
May 2, 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
30 changes: 21 additions & 9 deletions x/ecocredit/server/core/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,33 +78,45 @@ func setupBase(t *testing.T) *baseSuite {
// supply/balance of "10.5" for both retired and tradable.
func (s baseSuite) setupClassProjectBatch(t *testing.T) (classId, projectId, batchDenom string) {
classId, projectId, batchDenom = "C01", "P01", "C01-20200101-20210101-01"
assert.NilError(t, s.stateStore.ClassTable().Insert(s.ctx, &api.Class{
cKey, err := s.stateStore.ClassTable().InsertReturningID(s.ctx, &api.Class{
Id: classId,
Admin: s.addr,
Metadata: "",
CreditTypeAbbrev: "C",
})
assert.NilError(t, err)

assert.NilError(t, s.stateStore.ClassIssuerTable().Insert(s.ctx, &api.ClassIssuer{
ClassKey: cKey,
Issuer: s.addr,
}))
assert.NilError(t, s.stateStore.ProjectTable().Insert(s.ctx, &api.Project{

pKey, err := s.stateStore.ProjectTable().InsertReturningID(s.ctx, &api.Project{
Id: projectId,
ClassKey: 1,
ClassKey: cKey,
Jurisdiction: "US-OR",
Metadata: "",
}))
assert.NilError(t, s.stateStore.BatchTable().Insert(s.ctx, &api.Batch{
ProjectKey: 1,
})
assert.NilError(t, err)

bKey, err := s.stateStore.BatchTable().InsertReturningID(s.ctx, &api.Batch{
ProjectKey: pKey,
Denom: batchDenom,
Issuer: s.addr,
Metadata: "",
StartDate: &timestamppb.Timestamp{Seconds: 2},
EndDate: &timestamppb.Timestamp{Seconds: 2},
}))
})
assert.NilError(t, err)

assert.NilError(t, s.stateStore.BatchSupplyTable().Insert(s.ctx, &api.BatchSupply{
BatchKey: 1,
BatchKey: bKey,
TradableAmount: "10.5",
RetiredAmount: "10.5",
CancelledAmount: "",
}))
assert.NilError(t, s.stateStore.BatchBalanceTable().Insert(s.ctx, &api.BatchBalance{
BatchKey: 1,
BatchKey: bKey,
Address: s.addr,
Tradable: "10.5",
Retired: "10.5",
Expand Down
131 changes: 130 additions & 1 deletion x/ecocredit/server/core/mint_batch_credits.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,138 @@ package core
import (
"context"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

api "github.com/regen-network/regen-ledger/api/regen/ecocredit/v1"
"github.com/regen-network/regen-ledger/x/ecocredit/core"
"github.com/regen-network/regen-ledger/x/ecocredit/server/utils"
)

// MintBatchCredits issues additional credits from an open batch.
func (k Keeper) MintBatchCredits(ctx context.Context, req *core.MsgMintBatchCredits) (*core.MsgMintBatchCreditsResponse, error) {
panic("implement me")
issuer, err := sdk.AccAddressFromBech32(req.Issuer)
if err != nil {
return nil, err
}

batch, err := k.stateStore.BatchTable().GetByDenom(ctx, req.BatchDenom)
if err != nil {
return nil, sdkerrors.ErrInvalidRequest.Wrapf("could not get batch with denom %s: %s", req.BatchDenom, err.Error())
}

if err := k.assertCanMintBatch(issuer, batch); err != nil {
return nil, sdkerrors.ErrInvalidRequest.Wrapf("unable to mint credits: %s", err.Error())
}

if err = k.stateStore.BatchOrigTxTable().Insert(ctx, &api.BatchOrigTx{
TxId: req.OriginTx.Id,
Typ: req.OriginTx.Typ,
Note: req.Note,
BatchDenom: req.BatchDenom,
}); err != nil {
return nil, err
}

ct, err := utils.GetCreditTypeFromBatchDenom(ctx, k.stateStore, batch.Denom)
if err != nil {
return nil, err
}
precision := ct.Precision
for _, iss := range req.Issuance {
sdkCtx := sdk.UnwrapSDKContext(ctx)
recipient, err := sdk.AccAddressFromBech32(iss.Recipient)
if err != nil {
return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid recipient address %s: %s", iss.Recipient, err.Error())
}
decs, err := utils.GetNonNegativeFixedDecs(precision, iss.TradableAmount, iss.RetiredAmount)
if err != nil {
return nil, err
}
tradable, retired := decs[0], decs[1]

balance, err := utils.GetBalance(ctx, k.stateStore.BatchBalanceTable(), recipient, batch.Key)
if err != nil {
return nil, err
}
supply, err := k.stateStore.BatchSupplyTable().Get(ctx, batch.Key)
if err != nil {
return nil, err
}
decs, err = utils.GetNonNegativeFixedDecs(precision, balance.Tradable, balance.Retired, supply.TradableAmount, supply.RetiredAmount)
if err != nil {
return nil, err
}

balanceTradable, balanceRetired := decs[0], decs[1]
supplyTradable, supplyRetired := decs[2], decs[3]

if !retired.IsZero() {
balanceRetired, err = balanceRetired.Add(retired)
if err != nil {
return nil, err
}
supplyRetired, err = supplyRetired.Add(retired)
if err != nil {
return nil, err
}
if err := sdkCtx.EventManager().EmitTypedEvent(&core.EventRetire{
Retirer: iss.Recipient,
BatchDenom: req.BatchDenom,
Amount: iss.RetiredAmount,
Jurisdiction: iss.RetirementJurisdiction,
}); err != nil {
return nil, err
}
balance.Retired = balanceRetired.String()
supply.RetiredAmount = supplyRetired.String()
}
if !tradable.IsZero() {
balanceTradable, err = balanceTradable.Add(tradable)
if err != nil {
return nil, err
}
supplyTradable, err = supplyTradable.Add(tradable)
if err != nil {
return nil, err
}
if err := sdkCtx.EventManager().EmitTypedEvent(&core.EventReceive{
Sender: req.Issuer,
Recipient: iss.Recipient,
BatchDenom: req.BatchDenom,
TradableAmount: iss.TradableAmount,
RetiredAmount: iss.RetiredAmount,
}); err != nil {
return nil, err
}
balance.Tradable = balanceTradable.String()
supply.TradableAmount = supplyTradable.String()
}
if err := k.stateStore.BatchBalanceTable().Save(ctx, balance); err != nil {
return nil, err
}
if err := k.stateStore.BatchSupplyTable().Update(ctx, supply); err != nil {
return nil, err
}
}

if err := sdk.UnwrapSDKContext(ctx).EventManager().EmitTypedEvent(&core.EventMintBatchCredits{
BatchDenom: batch.Denom,
OriginTx: req.OriginTx,
}); err != nil {
return nil, err
}

return &core.MsgMintBatchCreditsResponse{}, nil
}

// asserts that the batch is open for minting and that the requester address matches the batch issuer address.
func (k Keeper) assertCanMintBatch(issuer sdk.AccAddress, batch *api.Batch) error {
if !batch.Open {
return sdkerrors.ErrInvalidRequest.Wrap("credits cannot be minted in a closed batch")
}
if !sdk.AccAddress(batch.Issuer).Equals(issuer) {
return sdkerrors.ErrUnauthorized.Wrapf("only the account that issued the batch can mint additional credits")
}
return nil
}
139 changes: 139 additions & 0 deletions x/ecocredit/server/core/mint_batch_credits_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package core

import (
"fmt"
"testing"

"github.com/cosmos/cosmos-sdk/orm/types/ormerrors"
sdk "github.com/cosmos/cosmos-sdk/types"
"gotest.tools/v3/assert"

api "github.com/regen-network/regen-ledger/api/regen/ecocredit/v1"
"github.com/regen-network/regen-ledger/types/math"
"github.com/regen-network/regen-ledger/x/ecocredit/core"
"github.com/regen-network/regen-ledger/x/ecocredit/server/utils"
)

func TestMintBatchCredits_Valid(t *testing.T) {
t.Parallel()
s := setupBase(t)
ctx := s.ctx
batch := setupMintBatchTest(s, true)

balBefore, err := s.stateStore.BatchBalanceTable().Get(ctx, s.addr, batch.Key)
assert.NilError(t, err)
supplyBefore, err := s.stateStore.BatchSupplyTable().Get(ctx, batch.Key)
assert.NilError(t, err)

mintTradable, mintRetired := math.NewDecFromInt64(10), math.NewDecFromInt64(10)
issuance := core.BatchIssuance{
Recipient: s.addr.String(),
TradableAmount: mintTradable.String(),
RetiredAmount: mintRetired.String(),
RetirementJurisdiction: "US-OR",
}
msg := core.MsgMintBatchCredits{
Issuer: s.addr.String(),
BatchDenom: batch.Denom,
Issuance: []*core.BatchIssuance{&issuance},
OriginTx: &core.OriginTx{
Typ: "Ethereum",
Id: "210985091248",
},
Note: "bridged credits",
}

_, err = s.k.MintBatchCredits(ctx, &msg)
assert.NilError(t, err)

balAfter, err := s.stateStore.BatchBalanceTable().Get(ctx, s.addr, batch.Key)
assert.NilError(t, err)
supplyAfter, err := s.stateStore.BatchSupplyTable().Get(ctx, batch.Key)
assert.NilError(t, err)

assertCreditsMinted(t, balBefore, balAfter, supplyBefore, supplyAfter, issuance, 6)
}

func TestMintBatchCredits_Unauthorized(t *testing.T) {
t.Parallel()
s := setupBase(t)
batch := setupMintBatchTest(s, true)
addr := sdk.AccAddress("foobar")

_, err := s.k.MintBatchCredits(s.ctx, &core.MsgMintBatchCredits{
Issuer: addr.String(),
BatchDenom: batch.Denom,
})
assert.ErrorContains(t, err, "unauthorized")
}

func TestMintBatchCredits_ClosedBatch(t *testing.T) {
t.Parallel()
s := setupBase(t)
batch := setupMintBatchTest(s, false)
addr := sdk.AccAddress("foobar")

_, err := s.k.MintBatchCredits(s.ctx, &core.MsgMintBatchCredits{
Issuer: addr.String(),
BatchDenom: batch.Denom,
})
assert.ErrorContains(t, err, "credits cannot be minted in a closed batch")
}

func TestMintBatchCredits_NotFound(t *testing.T) {
t.Parallel()
s := setupBase(t)
setupMintBatchTest(s, true)
addr := sdk.AccAddress("foobar")

_, err := s.k.MintBatchCredits(s.ctx, &core.MsgMintBatchCredits{
Issuer: addr.String(),
BatchDenom: "C05-00000000-00000000-001",
})
assert.ErrorContains(t, err, ormerrors.NotFound.Error())
}

func setupMintBatchTest(s *baseSuite, open bool) *api.Batch {
ctx := s.ctx
_, _, batchDenom := s.setupClassProjectBatch(s.t)
batch, err := s.stateStore.BatchTable().GetByDenom(ctx, batchDenom)
assert.NilError(s.t, err)
batch.Open = open
assert.NilError(s.t, s.stateStore.BatchTable().Update(ctx, batch))
return batch
}

func assertCreditsMinted(t *testing.T, balBefore, balAfter *api.BatchBalance, supBefore, supAfter *api.BatchSupply, issuance core.BatchIssuance, precision uint32) {
checkFunc := func(before, after, change math.Dec) {
expected, err := before.Add(change)
assert.NilError(t, err)
assert.Check(t, after.Equal(expected), fmt.Sprintf("expected %s got %s", expected.String(), after.String()))
}

issuanceDecs, err := utils.GetNonNegativeFixedDecs(precision, issuance.TradableAmount, issuance.RetiredAmount)
assert.NilError(t, err)
tradable, retired := issuanceDecs[0], issuanceDecs[1]

tradableBefore, retiredBefore, _ := extractBalanceDecs(t, balBefore, precision)
tradableAfter, retiredAfter, _ := extractBalanceDecs(t, balAfter, precision)
checkFunc(tradableBefore, tradableAfter, tradable)
checkFunc(retiredBefore, retiredAfter, retired)

supTBefore, supRBefore, _ := extractSupplyDecs(t, supBefore, precision)
supTAfter, supRAfter, _ := extractSupplyDecs(t, supAfter, precision)
checkFunc(supTBefore, supTAfter, tradable)
checkFunc(supRBefore, supRAfter, retired)

}

func extractBalanceDecs(t *testing.T, b *api.BatchBalance, precision uint32) (tradable, retired, escrowed math.Dec) {
decs, err := utils.GetNonNegativeFixedDecs(precision, b.Tradable, b.Retired, b.Escrowed)
assert.NilError(t, err)
return decs[0], decs[1], decs[2]
}

func extractSupplyDecs(t *testing.T, s *api.BatchSupply, precision uint32) (tradable, retired, cancelled math.Dec) {
decs, err := utils.GetNonNegativeFixedDecs(precision, s.TradableAmount, s.RetiredAmount, s.CancelledAmount)
assert.NilError(t, err)
return decs[0], decs[1], decs[2]
}
15 changes: 2 additions & 13 deletions x/ecocredit/server/core/query_balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package core
import (
"context"

"github.com/cosmos/cosmos-sdk/orm/types/ormerrors"
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/regen-network/regen-ledger/x/ecocredit/core"
"github.com/regen-network/regen-ledger/x/ecocredit/server/utils"
)

// Balance queries the balance (both tradable and retired) of a given credit
Expand All @@ -22,19 +22,8 @@ func (k Keeper) Balance(ctx context.Context, req *core.QueryBalanceRequest) (*co
return nil, err
}

balance, err := k.stateStore.BatchBalanceTable().Get(ctx, addr, batch.Key)
balance, err := utils.GetBalance(ctx, k.stateStore.BatchBalanceTable(), addr, batch.Key)
if err != nil {
if ormerrors.IsNotFound(err) {
return &core.QueryBalanceResponse{
Balance: &core.BatchBalanceInfo{
Address: addr.String(),
BatchDenom: batch.Denom,
Tradable: "0",
Retired: "0",
Escrowed: "0",
},
}, nil
}
return nil, err
}

Expand Down
15 changes: 3 additions & 12 deletions x/ecocredit/server/core/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

ecoApi "github.com/regen-network/regen-ledger/api/regen/ecocredit/v1"
"github.com/regen-network/regen-ledger/types/math"
"github.com/regen-network/regen-ledger/x/ecocredit/server/utils"

"github.com/cosmos/cosmos-sdk/orm/types/ormerrors"
sdk "github.com/cosmos/cosmos-sdk/types"
Expand All @@ -26,19 +27,9 @@ func (k Keeper) assertClassIssuer(goCtx context.Context, classID uint64, addr sd

// AddAndSaveBalance adds 'amt' to the addr's tradable balance.
func AddAndSaveBalance(ctx context.Context, table ecoApi.BatchBalanceTable, addr sdk.AccAddress, batchKey uint64, amt math.Dec) error {
bal, err := table.Get(ctx, addr, batchKey)
bal, err := utils.GetBalance(ctx, table, addr, batchKey)
if err != nil {
if ormerrors.IsNotFound(err) {
bal = &ecoApi.BatchBalance{
BatchKey: batchKey,
Address: addr,
Tradable: "0",
Retired: "0",
Escrowed: "0",
}
} else {
return err
}
return err
}
tradable, err := math.NewDecFromString(bal.Tradable)
if err != nil {
Expand Down
Loading