diff --git a/types/math/dec.go b/types/math/dec.go index 887bcf8db0..47ecbb14c1 100644 --- a/types/math/dec.go +++ b/types/math/dec.go @@ -90,6 +90,13 @@ func NewDecFromInt64(x int64) Dec { return res } +// NewDecFinite returns a decimal with a value of coeff * 10^exp. +func NewDecFinite(coeff int64, exp int32) Dec { + var res Dec + res.dec.SetFinite(coeff, exp) + return res +} + // Add returns a new Dec with value `x+y` without mutating any argument and error if // there is an overflow. func (x Dec) Add(y Dec) (Dec, error) { @@ -114,6 +121,19 @@ func (x Dec) Quo(y Dec) (Dec, error) { return z, errors.Wrap(err, "decimal quotient error") } +// MulExact returns a new dec with value x * y. The product must not round or an error will be returned. +func (x Dec) MulExact(y Dec) (Dec, error) { + var z Dec + condition, err := dec128Context.Mul(&z.dec, &x.dec, &y.dec) + if err != nil { + return z, err + } + if condition.Rounded() { + return z, errors.Wrap(err, "exact decimal product error") + } + return z, nil +} + // QuoInteger returns a new integral Dec with value `x/y` (formatted as decimal128, with 34 digit precision) // without mutating any argument and error if there is an overflow. func (x Dec) QuoInteger(y Dec) (Dec, error) { diff --git a/x/ecocredit/basket/keys.go b/x/ecocredit/basket/keys.go new file mode 100644 index 0000000000..f520cb6d04 --- /dev/null +++ b/x/ecocredit/basket/keys.go @@ -0,0 +1,5 @@ +package basket + +import "github.com/regen-network/regen-ledger/x/ecocredit" + +const BasketSubModuleName = ecocredit.ModuleName + "-basket" diff --git a/x/ecocredit/server/data_prefixes_test.go b/x/ecocredit/data_prefixes_test.go similarity index 93% rename from x/ecocredit/server/data_prefixes_test.go rename to x/ecocredit/data_prefixes_test.go index 897336503a..f295b380ba 100644 --- a/x/ecocredit/server/data_prefixes_test.go +++ b/x/ecocredit/data_prefixes_test.go @@ -1,4 +1,4 @@ -package server +package ecocredit import ( "testing" @@ -11,7 +11,7 @@ import ( var addr = sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) func TestKeys(t *testing.T) { - batchDenom := batchDenomT("testing-denom") + batchDenom := BatchDenomT("testing-denom") // tradable-balance-key key := TradableBalanceKey(addr, batchDenom) diff --git a/x/ecocredit/keys.go b/x/ecocredit/keys.go index 4b3dcfd58e..a89d77b70f 100644 --- a/x/ecocredit/keys.go +++ b/x/ecocredit/keys.go @@ -1,8 +1,166 @@ package ecocredit +import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/regen-network/regen-ledger/types/math" +) + const ( // ModuleName is the module name constant used in many places ModuleName = "ecocredit" DefaultParamspace = ModuleName + + TradableBalancePrefix byte = 0x0 + TradableSupplyPrefix byte = 0x1 + RetiredBalancePrefix byte = 0x2 + RetiredSupplyPrefix byte = 0x3 + CreditTypeSeqTablePrefix byte = 0x4 + ClassInfoTablePrefix byte = 0x5 + BatchInfoTablePrefix byte = 0x6 + ORMPrefix byte = 0x7 ) + +// BatchDenomT is used to prevent errors when forming keys as accounts and denoms are +// both represented as strings +type BatchDenomT string + +// - 0x0 : TradableBalance +// - 0x1 : TradableSupply +// - 0x2 : RetiredBalance +// - 0x3 : RetiredSupply + +// TradableBalanceKey creates the index key for recipient address and batch-denom +func TradableBalanceKey(acc sdk.AccAddress, denom BatchDenomT) []byte { + key := []byte{TradableBalancePrefix} + key = append(key, address.MustLengthPrefix(acc)...) + return append(key, denom...) +} + +// ParseBalanceKey parses the recipient address and batch-denom from tradable or retired balance key. +// Balance keys take the following form: +func ParseBalanceKey(key []byte) (sdk.AccAddress, BatchDenomT) { + addrLen := key[1] + addr := sdk.AccAddress(key[2 : 2+addrLen]) + return addr, BatchDenomT(key[2+addrLen:]) +} + +// TradableSupplyKey creates the tradable supply key for a given batch-denom +func TradableSupplyKey(batchDenom BatchDenomT) []byte { + key := []byte{TradableSupplyPrefix} + return append(key, batchDenom...) +} + +// ParseSupplyKey parses the batch-denom from tradable or retired supply key +func ParseSupplyKey(key []byte) BatchDenomT { + return BatchDenomT(key[1:]) +} + +// RetiredBalanceKey creates the index key for recipient address and batch-denom +func RetiredBalanceKey(acc sdk.AccAddress, batchDenom BatchDenomT) []byte { + key := []byte{RetiredBalancePrefix} + key = append(key, address.MustLengthPrefix(acc)...) + return append(key, batchDenom...) +} + +// RetiredSupplyKey creates the retired supply key for a given batch-denom +func RetiredSupplyKey(batchDenom BatchDenomT) []byte { + key := []byte{RetiredSupplyPrefix} + return append(key, batchDenom...) +} + +// GetDecimal retrieves a decimal by `key` from the given `store` +func GetDecimal(store sdk.KVStore, key []byte) (math.Dec, error) { + bz := store.Get(key) + if bz == nil { + return math.NewDecFromInt64(0), nil + } + + value, err := math.NewDecFromString(string(bz)) + if err != nil { + return math.Dec{}, sdkerrors.Wrap(err, fmt.Sprintf("can't unmarshal %s as decimal", bz)) + } + + return value, nil +} + +// SetDecimal stores a decimal by `key` in the given `store` +func SetDecimal(store sdk.KVStore, key []byte, value math.Dec) { + // always remove all trailing zeros for canonical representation + value, _ = value.Reduce() + + if value.IsZero() { + store.Delete(key) + } else { + // use floating notation here always for canonical representation + store.Set(key, []byte(value.String())) + } +} + +// AddAndSetDecimal retrieves a decimal from the given key, adds it to x, and saves it. +func AddAndSetDecimal(store sdk.KVStore, key []byte, x math.Dec) error { + value, err := GetDecimal(store, key) + if err != nil { + return err + } + + value, err = value.Add(x) + if err != nil { + return err + } + + SetDecimal(store, key, value) + return nil +} + +// SubAndSetDecimal retrieves a decimal from the given key, subtracts x from it, and saves it. +func SubAndSetDecimal(store sdk.KVStore, key []byte, x math.Dec) error { + value, err := GetDecimal(store, key) + if err != nil { + return err + } + + if value.Cmp(x) == -1 { + return ErrInsufficientFunds + } + + value, err = math.SafeSubBalance(value, x) + if err != nil { + return err + } + + SetDecimal(store, key, value) + return nil +} + +// IterateSupplies iterates over supplies and calls the specified callback function `cb` +func IterateSupplies(store sdk.KVStore, storeKey byte, cb func(denom, supply string) (bool, error)) error { + iter := sdk.KVStorePrefixIterator(store, []byte{storeKey}) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + stop, err := cb(string(ParseSupplyKey(iter.Key())), string(iter.Value())) + if err != nil { + return err + } + if stop { + break + } + } + + return nil +} + +// IterateBalances iterates over balances and calls the specified callback function `cb` +func IterateBalances(store sdk.KVStore, storeKey byte, cb func(address, denom, balance string) bool) { + iter := sdk.KVStorePrefixIterator(store, []byte{storeKey}) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + addr, denom := ParseBalanceKey(iter.Key()) + if cb(addr.String(), string(denom), string(iter.Value())) { + break + } + } +} diff --git a/x/ecocredit/server/basket/keeper.go b/x/ecocredit/server/basket/keeper.go index ba39c4aa17..2a4e8d2e3e 100644 --- a/x/ecocredit/server/basket/keeper.go +++ b/x/ecocredit/server/basket/keeper.go @@ -16,18 +16,19 @@ type Keeper struct { stateStore basketv1.StateStore bankKeeper BankKeeper ecocreditKeeper EcocreditKeeper + storeKey sdk.StoreKey } var _ baskettypes.MsgServer = Keeper{} var _ baskettypes.QueryServer = Keeper{} // NewKeeper returns a new keeper instance. -func NewKeeper(db ormdb.ModuleDB, ecocreditKeeper EcocreditKeeper, bankKeeper BankKeeper) Keeper { +func NewKeeper(db ormdb.ModuleDB, ecocreditKeeper EcocreditKeeper, bankKeeper BankKeeper, storeKey sdk.StoreKey) Keeper { basketStore, err := basketv1.NewStateStore(db) if err != nil { panic(err) } - return Keeper{bankKeeper: bankKeeper, ecocreditKeeper: ecocreditKeeper, stateStore: basketStore} + return Keeper{bankKeeper: bankKeeper, ecocreditKeeper: ecocreditKeeper, stateStore: basketStore, storeKey: storeKey} } // EcocreditKeeper abstracts over methods that the main eco-credit keeper diff --git a/x/ecocredit/server/basket/keeper_test.go b/x/ecocredit/server/basket/keeper_test.go index 80c8e41a06..0d982ddd29 100644 --- a/x/ecocredit/server/basket/keeper_test.go +++ b/x/ecocredit/server/basket/keeper_test.go @@ -1,6 +1,7 @@ package basket_test import ( + sdk "github.com/cosmos/cosmos-sdk/types" "testing" "github.com/cosmos/cosmos-sdk/orm/model/ormdb" @@ -19,6 +20,7 @@ func TestKeeperExample(t *testing.T) { bankKeeper := mocks.NewMockBankKeeper(ctrl) ecocreditKeeper := mocks.NewMockEcocreditKeeper(ctrl) - k := basket.NewKeeper(db, ecocreditKeeper, bankKeeper) + sk := sdk.NewKVStoreKey("test") + k := basket.NewKeeper(db, ecocreditKeeper, bankKeeper, sk) require.NotNil(t, k) } diff --git a/x/ecocredit/server/basket/put.go b/x/ecocredit/server/basket/put.go index 6f2f2b05de..4547bd3ce9 100644 --- a/x/ecocredit/server/basket/put.go +++ b/x/ecocredit/server/basket/put.go @@ -2,11 +2,198 @@ package basket import ( "context" + "time" + "github.com/cosmos/cosmos-sdk/orm/types/ormerrors" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + basketv1 "github.com/regen-network/regen-ledger/api/regen/ecocredit/basket/v1" + regenmath "github.com/regen-network/regen-ledger/types/math" + "github.com/regen-network/regen-ledger/x/ecocredit" baskettypes "github.com/regen-network/regen-ledger/x/ecocredit/basket" + + "google.golang.org/protobuf/types/known/timestamppb" ) -func (k Keeper) Put(ctx context.Context, put *baskettypes.MsgPut) (*baskettypes.MsgPutResponse, error) { - //TODO implement me - panic("implement me") +// Put deposits ecocredits into a basket, returning fungible coins to the depositor. +// NOTE: the credits MUST adhere to the following specifications set by the basket: credit type, class, and date criteria. +func (k Keeper) Put(ctx context.Context, req *baskettypes.MsgPut) (*baskettypes.MsgPutResponse, error) { + ownerAddr, err := sdk.AccAddressFromBech32(req.Owner) + if err != nil { + return nil, err + } + + // get the basket + basket, err := k.stateStore.BasketStore().GetByBasketDenom(ctx, req.BasketDenom) + if err != nil { + return nil, err + } + + // keep track of the total amount of tokens to give to the depositor + amountReceived := sdk.NewInt(0) + for _, credit := range req.Credits { + // get credit batch info + res, err := k.ecocreditKeeper.BatchInfo(ctx, &ecocredit.QueryBatchInfoRequest{BatchDenom: credit.BatchDenom}) + if err != nil { + return nil, err + } + batchInfo := res.Info + + // validate that the credit batch adheres to the basket's specifications + if err := k.canBasketAcceptCredit(ctx, basket, batchInfo); err != nil { + return nil, err + } + // get the amount of credits in dec + amt, err := regenmath.NewPositiveFixedDecFromString(credit.Amount, basket.Exponent) + if err != nil { + return nil, err + } + // update the user and basket balances + if err = k.transferToBasket(ctx, ownerAddr, amt, basket, batchInfo); err != nil { + return nil, err + } + // get the amount of basket tokens to give to the depositor + tokens, err := creditAmountToBasketCoins(amt, basket.Exponent, basket.BasketDenom) + if err != nil { + return nil, err + } + // update the total amount received so far + amountReceived = amountReceived.Add(tokens[0].Amount) + } + + // mint and send tokens to depositor + coinsToSend := sdk.Coins{sdk.NewCoin(basket.BasketDenom, amountReceived)} + sdkCtx := sdk.UnwrapSDKContext(ctx) + if err = k.bankKeeper.MintCoins(sdkCtx, baskettypes.BasketSubModuleName, coinsToSend); err != nil { + return nil, err + } + if err = k.bankKeeper.SendCoinsFromModuleToAccount(sdkCtx, baskettypes.BasketSubModuleName, ownerAddr, coinsToSend); err != nil { + return nil, err + } + + if err = sdkCtx.EventManager().EmitTypedEvent(&baskettypes.EventPut{ + Owner: ownerAddr.String(), + BasketDenom: basket.BasketDenom, + Credits: req.Credits, + Amount: amountReceived.String(), + }); err != nil { + return nil, err + } + + return &baskettypes.MsgPutResponse{AmountReceived: amountReceived.String()}, nil +} + +// canBasketAcceptCredit checks that a credit adheres to the specifications of a basket. Specifically, it checks: +// - batch's start time is within the basket's specified time window or min start date +// - class is in the basket's allowed class store +// - type matches the baskets specified credit type. +func (k Keeper) canBasketAcceptCredit(ctx context.Context, basket *basketv1.Basket, batchInfo *ecocredit.BatchInfo) error { + sdkCtx := sdk.UnwrapSDKContext(ctx) + blockTime := sdkCtx.BlockTime() + errInvalidReq := sdkerrors.ErrInvalidAddress + + if basket.DateCriteria != nil && basket.DateCriteria.Sum != nil { + // check time window match + var minStartDate time.Time + switch criteria := basket.DateCriteria.Sum.(type) { + case *basketv1.DateCriteria_MinStartDate: + minStartDate = criteria.MinStartDate.AsTime() + case *basketv1.DateCriteria_StartDateWindow: + window := criteria.StartDateWindow.AsDuration() + minStartDate = blockTime.Add(-window) + } + + if batchInfo.StartDate.Before(minStartDate) { + return errInvalidReq.Wrapf("cannot put a credit from a batch with start date %s "+ + "into a basket that requires an earliest start date of %s", batchInfo.StartDate.String(), minStartDate.String()) + } + + } + + // check credit class match + found, err := k.stateStore.BasketClassStore().Has(ctx, basket.Id, batchInfo.ClassId) + if err != nil { + return err + } + if !found { + return errInvalidReq.Wrapf("credit class %s is not allowed in this basket", batchInfo.ClassId) + } + + // check credit type match + requiredCreditType := basket.CreditTypeName + res2, err := k.ecocreditKeeper.ClassInfo(ctx, &ecocredit.QueryClassInfoRequest{ClassId: batchInfo.ClassId}) + if err != nil { + return err + } + gotCreditType := res2.Info.CreditType.Name + if requiredCreditType != gotCreditType { + return errInvalidReq.Wrapf("cannot use credit of type %s in a basket that requires credit type %s", gotCreditType, requiredCreditType) + } + return nil +} + +// transferToBasket updates the balance of the user in the legacy KVStore as well as the basket's balance in the ORM. +func (k Keeper) transferToBasket(ctx context.Context, sender sdk.AccAddress, amt regenmath.Dec, basket *basketv1.Basket, batchInfo *ecocredit.BatchInfo) error { + sdkCtx := sdk.UnwrapSDKContext(ctx) + store := sdkCtx.KVStore(k.storeKey) + + // update the user balance + userBalanceKey := ecocredit.TradableBalanceKey(sender, ecocredit.BatchDenomT(batchInfo.BatchDenom)) + userBalance, err := ecocredit.GetDecimal(store, userBalanceKey) + if err != nil { + return err + } + newUserBalance, err := regenmath.SafeSubBalance(userBalance, amt) + if err != nil { + return err + } + ecocredit.SetDecimal(store, userBalanceKey, newUserBalance) + + // update basket balance with amount sent + var bal *basketv1.BasketBalance + bal, err = k.stateStore.BasketBalanceStore().Get(ctx, basket.Id, batchInfo.BatchDenom) + if err != nil { + if ormerrors.IsNotFound(err) { + bal = &basketv1.BasketBalance{ + BasketId: basket.Id, + BatchDenom: batchInfo.BatchDenom, + Balance: amt.String(), + BatchStartDate: timestamppb.New(*batchInfo.StartDate), + } + } else { + return err + } + } else { + newBalance, err := regenmath.NewPositiveFixedDecFromString(bal.Balance, basket.Exponent) + if err != nil { + return err + } + newBalance, err = newBalance.Add(amt) + if err != nil { + return err + } + bal.Balance = newBalance.String() + } + if err = k.stateStore.BasketBalanceStore().Save(ctx, bal); err != nil { + return err + } + return nil +} + +// creditAmountToBasketCoins calculates the tokens to award to the depositor +func creditAmountToBasketCoins(creditAmt regenmath.Dec, exp uint32, denom string) (sdk.Coins, error) { + var coins sdk.Coins + multiplier := regenmath.NewDecFinite(1, int32(exp)) + tokenAmt, err := multiplier.MulExact(creditAmt) + if err != nil { + return coins, err + } + + i64Amt, err := tokenAmt.Int64() + if err != nil { + return coins, err + } + + return sdk.Coins{sdk.NewCoin(denom, sdk.NewInt(i64Amt))}, nil } diff --git a/x/ecocredit/server/basket/put_test.go b/x/ecocredit/server/basket/put_test.go new file mode 100644 index 0000000000..b1ed1d2e25 --- /dev/null +++ b/x/ecocredit/server/basket/put_test.go @@ -0,0 +1,386 @@ +package basket_test + +import ( + "context" + "github.com/cosmos/cosmos-sdk/orm/model/ormdb" + "github.com/cosmos/cosmos-sdk/orm/model/ormtable" + "github.com/cosmos/cosmos-sdk/orm/testing/ormtest" + "github.com/cosmos/cosmos-sdk/orm/types/ormerrors" + "github.com/cosmos/cosmos-sdk/store" + "github.com/cosmos/cosmos-sdk/store/types" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/golang/mock/gomock" + basketv1 "github.com/regen-network/regen-ledger/api/regen/ecocredit/basket/v1" + "github.com/regen-network/regen-ledger/orm" + "github.com/regen-network/regen-ledger/types/math" + "github.com/regen-network/regen-ledger/x/ecocredit" + basket2 "github.com/regen-network/regen-ledger/x/ecocredit/basket" + "github.com/regen-network/regen-ledger/x/ecocredit/server" + "github.com/regen-network/regen-ledger/x/ecocredit/server/basket" + "github.com/regen-network/regen-ledger/x/ecocredit/server/basket/mocks" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/log" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + dbm "github.com/tendermint/tm-db" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + "testing" + "time" +) + +func TestPut(t *testing.T) { + basketDenom := "BASKET" + basketDenom2 := "BASKET2" + classId := "C02" + startDate, err := time.Parse("2006-01-02", "2020-01-01") + require.NoError(t, err) + endDate, err := time.Parse("2006-01-02", "2021-01-01") + require.NoError(t, err) + denom, err := ecocredit.FormatDenom(classId, 1, &startDate, &endDate) + require.NoError(t, err) + testClassInfo := ecocredit.ClassInfo{ + ClassId: classId, + Admin: "somebody", + Issuers: nil, + Metadata: nil, + CreditType: &ecocredit.CreditType{ + Name: "carbon", + Abbreviation: "C", + Unit: "many carbons", + Precision: 6, + }, + NumBatches: 1, + } + classInfoRes := ecocredit.QueryClassInfoResponse{Info: &testClassInfo} + testBatchInfo := ecocredit.BatchInfo{ + ClassId: classId, + BatchDenom: denom, + Issuer: "somebody", + TotalAmount: "1000000000000000000000000000", + Metadata: nil, + AmountCancelled: "0", + StartDate: &startDate, + EndDate: &endDate, + ProjectLocation: "US-NY", + } + batchInfoRes := ecocredit.QueryBatchInfoResponse{Info: &testBatchInfo} + + + ctx := context.Background() + _,_, addr := testdata.KeyTestPubAddr() + ctrl := gomock.NewController(t) + b := ormtest.NewMemoryBackend() + db, err := ormdb.NewModuleDB(server.ModuleSchema, ormdb.ModuleDBOptions{ + GetBackend: func(ctx context.Context) (ormtable.Backend, error) { + return b, nil + }, + GetReadBackend: func(ctx context.Context) (ormtable.ReadBackend, error) { + return b, nil + }, + }) + require.NoError(t, err) + basketTbl := db.GetTable(&basketv1.Basket{}) + err = basketTbl.Insert(ctx, &basketv1.Basket{ + BasketDenom: basketDenom, + DisableAutoRetire: true, + CreditTypeName: "carbon", + DateCriteria: &basketv1.DateCriteria{Sum: &basketv1.DateCriteria_MinStartDate{MinStartDate: timestamppb.New(startDate)}}, + Exponent: 6, + }) + require.NoError(t, err) + basketBalanceTbl := db.GetTable(&basketv1.BasketBalance{}) + var dur time.Duration = 500000000000000000 + validStartDateWindow := startDate.Add(-dur) + err = basketTbl.Insert(ctx, &basketv1.Basket{ + BasketDenom: basketDenom2 , + DisableAutoRetire: true, + CreditTypeName: "carbon", + DateCriteria: &basketv1.DateCriteria{Sum: &basketv1.DateCriteria_StartDateWindow{StartDateWindow: durationpb.New(dur)}}, + Exponent: 6, + }) + require.NoError(t, err) + basketDenomToId := make(map[string]uint64) + basketDenomToId[basketDenom] = 1 + basketDenomToId[basketDenom2] = 2 + bsktClsTbl := db.GetTable(&basketv1.BasketClass{}) + err = bsktClsTbl.Insert(ctx, &basketv1.BasketClass{ + BasketId: 1, + ClassId: classId, + }) + require.NoError(t, err) + err = bsktClsTbl.Insert(ctx, &basketv1.BasketClass{ + BasketId: 2, + ClassId: classId, + }) + require.NoError(t, err) + + bankKeeper := mocks.NewMockBankKeeper(ctrl) + ecocreditKeeper := mocks.NewMockEcocreditKeeper(ctrl) + sk := sdk.NewKVStoreKey("test") + k := basket.NewKeeper(db, ecocreditKeeper, bankKeeper, sk) + require.NotNil(t, k) + + sdkCtx := sdkContextForStoreKey(sk).WithContext(ctx).WithBlockTime(startDate) + ctx = sdk.WrapSDKContext(sdkCtx) + sdkCtx = ctx.Value(sdk.SdkContextKey).(sdk.Context) + + testCases := []struct{ + name string + startingBalance string + msg basket2.MsgPut + expectedBasketCoins string + expectCalls func() + errMsg string + }{ + { + name: "valid case", + startingBalance: "100000000", + msg: basket2.MsgPut{ + Owner: addr.String(), + BasketDenom: basketDenom, + Credits: []*basket2.BasketCredit{{BatchDenom: denom, Amount: "2"}}, + }, + expectedBasketCoins: "2000000", // 2 million + expectCalls: func() { + ecocreditKeeper.EXPECT(). + BatchInfo(ctx, &ecocredit.QueryBatchInfoRequest{BatchDenom: denom}). + Return(&batchInfoRes, nil) + + ecocreditKeeper.EXPECT(). + ClassInfo(ctx, &ecocredit.QueryClassInfoRequest{ClassId: classId}). + Return(&classInfoRes, nil) + + coinAward := sdk.NewCoins(sdk.NewCoin(basketDenom, sdk.NewInt(2_000_000))) + bankKeeper.EXPECT(). + MintCoins(sdkCtx, basket2.BasketSubModuleName, coinAward). + Return(nil) + + bankKeeper.EXPECT(). + SendCoinsFromModuleToAccount(sdkCtx, basket2.BasketSubModuleName, addr, coinAward). + Return(nil) + }, + }, + { + name: "valid case - basket 2 with rolling window", + startingBalance: "100000000", + msg: basket2.MsgPut{ + Owner: addr.String(), + BasketDenom: basketDenom2, + Credits: []*basket2.BasketCredit{{BatchDenom: denom, Amount: "2"}}, + }, + expectedBasketCoins: "2000000", // 2 million + expectCalls: func() { + ecocreditKeeper.EXPECT(). + BatchInfo(ctx, &ecocredit.QueryBatchInfoRequest{BatchDenom: denom}). + Return(&batchInfoRes, nil) + + ecocreditKeeper.EXPECT(). + ClassInfo(ctx, &ecocredit.QueryClassInfoRequest{ClassId: classId}). + Return(&classInfoRes, nil) + + coinAward := sdk.NewCoins(sdk.NewCoin(basketDenom2, sdk.NewInt(2_000_000))) + bankKeeper.EXPECT(). + MintCoins(sdkCtx, basket2.BasketSubModuleName, coinAward). + Return(nil) + + bankKeeper.EXPECT(). + SendCoinsFromModuleToAccount(sdkCtx, basket2.BasketSubModuleName, addr, coinAward). + Return(nil) + }, + }, + { + name: "insufficient funds", + startingBalance: "1", + msg: basket2.MsgPut{ + Owner: addr.String(), + BasketDenom: basketDenom, + Credits: []*basket2.BasketCredit{{BatchDenom: denom, Amount: "2"}}, + }, + expectedBasketCoins: "2000000", // 2 million + expectCalls: func() { + ecocreditKeeper.EXPECT(). + BatchInfo(ctx, &ecocredit.QueryBatchInfoRequest{BatchDenom: denom}). + Return(&batchInfoRes, nil) + + ecocreditKeeper.EXPECT(). + ClassInfo(ctx, &ecocredit.QueryClassInfoRequest{ClassId: classId}). + Return(&classInfoRes, nil) + + }, + errMsg: "insufficient funds", + }, + { + name: "basket not found", + startingBalance: "1", + msg: basket2.MsgPut{ + Owner: addr.String(), + BasketDenom: "FooBar", + Credits: []*basket2.BasketCredit{{BatchDenom: denom, Amount: "2"}}, + }, + expectedBasketCoins: "2000000", // 2 million + expectCalls: func() { + }, + errMsg: ormerrors.NotFound.Error(), + }, + { + name: "batch not found", + startingBalance: "20", + msg: basket2.MsgPut{ + Owner: addr.String(), + BasketDenom: basketDenom, + Credits: []*basket2.BasketCredit{{BatchDenom: "FooBarBaz", Amount: "2"}}, + }, + expectedBasketCoins: "2000000", // 2 million + expectCalls: func() { + ecocreditKeeper.EXPECT(). + BatchInfo(ctx, &ecocredit.QueryBatchInfoRequest{BatchDenom: "FooBarBaz"}). + Return(nil, orm.ErrNotFound) + }, + errMsg: orm.ErrNotFound.Error(), + }, + { + name: "class not allowed", + startingBalance: "100000000", + msg: basket2.MsgPut{ + Owner: addr.String(), + BasketDenom: basketDenom, + Credits: []*basket2.BasketCredit{{BatchDenom: "blah", Amount: "2"}}, + }, + expectedBasketCoins: "2000000", // 2 million + expectCalls: func() { + badInfo := *batchInfoRes.Info + badInfo.ClassId = "blah01" + ecocreditKeeper.EXPECT(). + BatchInfo(ctx, &ecocredit.QueryBatchInfoRequest{BatchDenom: "blah"}). + Return(&ecocredit.QueryBatchInfoResponse{Info: &badInfo}, nil) + }, + errMsg: "credit class blah01 is not allowed in this basket", + }, + { + name: "wrong credit type", + startingBalance: "100000000", + msg: basket2.MsgPut{ + Owner: addr.String(), + BasketDenom: basketDenom, + Credits: []*basket2.BasketCredit{{BatchDenom: denom, Amount: "2"}}, + }, + expectedBasketCoins: "2000000", // 2 million + expectCalls: func() { + ecocreditKeeper.EXPECT(). + BatchInfo(ctx, &ecocredit.QueryBatchInfoRequest{BatchDenom: denom}). + Return(&batchInfoRes, nil) + + badClass := *classInfoRes.Info + badClass.CreditType.Name = "BadType" + ecocreditKeeper.EXPECT(). + ClassInfo(ctx, &ecocredit.QueryClassInfoRequest{ClassId: classId}). + Return(&ecocredit.QueryClassInfoResponse{Info: &badClass}, nil) + }, + errMsg: "cannot use credit of type BadType in a basket that requires credit type carbon", + }, + { + name: "batch out of time window", + startingBalance: "100000000", + msg: basket2.MsgPut{ + Owner: addr.String(), + BasketDenom: basketDenom, + Credits: []*basket2.BasketCredit{{BatchDenom: denom, Amount: "2"}}, + }, + expectedBasketCoins: "2000000", // 2 million + expectCalls: func() { + badTime, err := time.Parse("2006-01-02", "1984-01-01") + require.NoError(t, err) + badTimeInfo := *batchInfoRes.Info + badTimeInfo.StartDate = &badTime + ecocreditKeeper.EXPECT(). + BatchInfo(ctx, &ecocredit.QueryBatchInfoRequest{BatchDenom: denom}). + Return(&ecocredit.QueryBatchInfoResponse{Info: &badTimeInfo}, nil) + + }, + errMsg: "cannot put a credit from a batch with start date", + }, + { + name: "batch outside of rolling time window", + startingBalance: "100000000", + msg: basket2.MsgPut{ + Owner: addr.String(), + BasketDenom: basketDenom2, + Credits: []*basket2.BasketCredit{{BatchDenom: denom, Amount: "2"}}, + }, + expectedBasketCoins: "2000000", // 2 million + expectCalls: func() { + badTimeInfo := *batchInfoRes.Info + bogusDur := time.Duration(999999999999999) + badTime := validStartDateWindow.Add(-bogusDur) + badTimeInfo.StartDate = &badTime + ecocreditKeeper.EXPECT(). + BatchInfo(ctx, &ecocredit.QueryBatchInfoRequest{BatchDenom: denom}). + Return(&ecocredit.QueryBatchInfoResponse{Info: &badTimeInfo}, nil) + + }, + errMsg: "cannot put a credit from a batch with start date", + }, + + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.expectCalls() + legacyStore := sdkCtx.KVStore(sk) + tradKey := ecocredit.TradableBalanceKey(addr, ecocredit.BatchDenomT(denom)) + userFunds, err := math.NewDecFromString(tc.startingBalance) + require.NoError(t, err) + ecocredit.SetDecimal(legacyStore, tradKey, userFunds) + res, err := k.Put(ctx, &tc.msg) + if tc.errMsg == "" { // no error! + require.NoError(t, err) + require.Equal(t, res.AmountReceived, tc.expectedBasketCoins) + for _, credit := range tc.msg.Credits { + assertUserSentCredits(t, userFunds, credit.Amount, tradKey, legacyStore) + assertBasketHasCredits(t, ctx, credit, basketDenomToId[tc.msg.BasketDenom], basketBalanceTbl) + } + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errMsg) + } + }) + } +} + +func assertBasketHasCredits(t *testing.T, ctx context.Context, credit *basket2.BasketCredit, basketID uint64, basketBalTbl ormtable.Table) { + basketBal := basketv1.BasketBalance{ + BasketId: basketID, + BatchDenom: credit.BatchDenom, + Balance: "", + BatchStartDate: nil, + } + found, err := basketBalTbl.Get(ctx, &basketBal) + require.NoError(t, err) + require.True(t, found) + require.Equal(t, basketBal.Balance, credit.Amount) +} + +func assertUserSentCredits(t *testing.T, oldBalance math.Dec, amountSent string, balanceKey []byte, store types.KVStore) { + amtSent, err := math.NewDecFromString(amountSent) + require.NoError(t, err) + currentBalance, err := ecocredit.GetDecimal(store, balanceKey) + require.NoError(t, err) + + checkBalance, err := currentBalance.Add(amtSent) + require.NoError(t, err) + + require.True(t, checkBalance.IsEqual(oldBalance)) +} + + +func sdkContextForStoreKey(key *types.KVStoreKey) sdk.Context { + db := dbm.NewMemDB() + cms := store.NewCommitMultiStore(db) + cms.MountStoreWithDB(key, sdk.StoreTypeIAVL, db) + err := cms.LoadLatestVersion() + if err != nil { + panic(err) + } + return sdk.NewContext(cms, tmproto.Header{}, false, log.NewNopLogger()) +} diff --git a/x/ecocredit/server/basket/query_balance_test.go b/x/ecocredit/server/basket/query_balance_test.go index 666684dbbc..83bf2aeb67 100644 --- a/x/ecocredit/server/basket/query_balance_test.go +++ b/x/ecocredit/server/basket/query_balance_test.go @@ -1,6 +1,7 @@ package basket_test import ( + sdk "github.com/cosmos/cosmos-sdk/types" "testing" baskettypes "github.com/regen-network/regen-ledger/x/ecocredit/basket" @@ -30,7 +31,8 @@ func TestKeeper_BasketBalance(t *testing.T) { require.NoError(t, err) bankKeeper := mocks.NewMockBankKeeper(ctrl) ecocreditKeeper := mocks.NewMockEcocreditKeeper(ctrl) - k := basket.NewKeeper(db, ecocreditKeeper, bankKeeper) + sk := sdk.NewKVStoreKey("test") + k := basket.NewKeeper(db, ecocreditKeeper, bankKeeper, sk) // add a basket basketDenom := "foo" diff --git a/x/ecocredit/server/basket/query_basket_test.go b/x/ecocredit/server/basket/query_basket_test.go index a7c4976828..11236696fc 100644 --- a/x/ecocredit/server/basket/query_basket_test.go +++ b/x/ecocredit/server/basket/query_basket_test.go @@ -1,6 +1,7 @@ package basket_test import ( + sdk "github.com/cosmos/cosmos-sdk/types" "testing" "github.com/golang/mock/gomock" @@ -30,7 +31,8 @@ func TestKeeper_Basket(t *testing.T) { require.NoError(t, err) bankKeeper := mocks.NewMockBankKeeper(ctrl) ecocreditKeeper := mocks.NewMockEcocreditKeeper(ctrl) - k := basket.NewKeeper(db, ecocreditKeeper, bankKeeper) + sk := sdk.NewKVStoreKey("test") + k := basket.NewKeeper(db, ecocreditKeeper, bankKeeper, sk) // add a basket basketDenom := "foo" diff --git a/x/ecocredit/server/basket/query_baskets_test.go b/x/ecocredit/server/basket/query_baskets_test.go index 1891eaa7a4..770ff6b59a 100644 --- a/x/ecocredit/server/basket/query_baskets_test.go +++ b/x/ecocredit/server/basket/query_baskets_test.go @@ -1,6 +1,7 @@ package basket_test import ( + sdk "github.com/cosmos/cosmos-sdk/types" "testing" "github.com/cosmos/cosmos-sdk/types/query" @@ -40,7 +41,8 @@ func TestQueryBaskets(t *testing.T) { require.NoError(t, err) bankKeeper := mocks.NewMockBankKeeper(ctrl) ecocreditKeeper := mocks.NewMockEcocreditKeeper(ctrl) - k := basket.NewKeeper(db, ecocreditKeeper, bankKeeper) + sk := sdk.NewKVStoreKey("test") + k := basket.NewKeeper(db, ecocreditKeeper, bankKeeper, sk) // query all res, err := k.Baskets(ctx, &baskettypes.QueryBasketsRequest{}) diff --git a/x/ecocredit/server/data_prefixes.go b/x/ecocredit/server/data_prefixes.go deleted file mode 100644 index bac5054cd8..0000000000 --- a/x/ecocredit/server/data_prefixes.go +++ /dev/null @@ -1,53 +0,0 @@ -package server - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/address" -) - -// batchDenomT is used to prevent errors when forming keys as accounts and denoms are -// both represented as strings -type batchDenomT string - -// - 0x0 : TradableBalance -// - 0x1 : TradableSupply -// - 0x2 : RetiredBalance -// - 0x3 : RetiredSupply - -// TradableBalanceKey creates the index key for recipient address and batch-denom -func TradableBalanceKey(acc sdk.AccAddress, denom batchDenomT) []byte { - key := []byte{TradableBalancePrefix} - key = append(key, address.MustLengthPrefix(acc)...) - return append(key, denom...) -} - -// ParseBalanceKey parses the recipient address and batch-denom from tradable or retired balance key. -func ParseBalanceKey(key []byte) (sdk.AccAddress, batchDenomT) { - addrLen := key[1] - addr := sdk.AccAddress(key[2 : 2+addrLen]) - return addr, batchDenomT(key[2+addrLen:]) -} - -// TradableSupplyKey creates the tradable supply key for a given batch-denom -func TradableSupplyKey(batchDenom batchDenomT) []byte { - key := []byte{TradableSupplyPrefix} - return append(key, batchDenom...) -} - -// ParseSupplyKey parses the batch-denom from tradable or retired supply key -func ParseSupplyKey(key []byte) batchDenomT { - return batchDenomT(key[1:]) -} - -// RetiredBalanceKey creates the index key for recipient address and batch-denom -func RetiredBalanceKey(acc sdk.AccAddress, batchDenom batchDenomT) []byte { - key := []byte{RetiredBalancePrefix} - key = append(key, address.MustLengthPrefix(acc)...) - return append(key, batchDenom...) -} - -// RetiredSupplyKey creates the retired supply key for a given batch-denom -func RetiredSupplyKey(batchDenom batchDenomT) []byte { - key := []byte{RetiredSupplyPrefix} - return append(key, batchDenom...) -} diff --git a/x/ecocredit/server/genesis.go b/x/ecocredit/server/genesis.go index 0778c355a3..9e8d1d5088 100644 --- a/x/ecocredit/server/genesis.go +++ b/x/ecocredit/server/genesis.go @@ -49,9 +49,9 @@ func (s serverImpl) InitGenesis(ctx types.Context, cdc codec.Codec, data json.Ra // validateSupplies returns an error if credit batch genesis supply does not equal to calculated supply. func validateSupplies(store sdk.KVStore, supplies []*ecocredit.Supply) error { - var denomT batchDenomT + var denomT ecocredit.BatchDenomT for _, supply := range supplies { - denomT = batchDenomT(supply.BatchDenom) + denomT = ecocredit.BatchDenomT(supply.BatchDenom) tradableSupply := math.NewDecFromInt64(0) retiredSupply := math.NewDecFromInt64(0) var err error @@ -62,7 +62,7 @@ func validateSupplies(store sdk.KVStore, supplies []*ecocredit.Supply) error { } } - tradable, err := getDecimal(store, TradableSupplyKey(denomT)) + tradable, err := ecocredit.GetDecimal(store, ecocredit.TradableSupplyKey(denomT)) if err != nil { return err } @@ -78,7 +78,7 @@ func validateSupplies(store sdk.KVStore, supplies []*ecocredit.Supply) error { } } - retired, err := getDecimal(store, RetiredSupplyKey(denomT)) + retired, err := ecocredit.GetDecimal(store, ecocredit.RetiredSupplyKey(denomT)) if err != nil { return err } @@ -98,7 +98,7 @@ func setBalanceAndSupply(store sdk.KVStore, balances []*ecocredit.Balance) error if err != nil { return err } - denomT := batchDenomT(balance.BatchDenom) + denomT := ecocredit.BatchDenomT(balance.BatchDenom) // set tradable balance and update supply if balance.TradableBalance != "" { @@ -106,11 +106,11 @@ func setBalanceAndSupply(store sdk.KVStore, balances []*ecocredit.Balance) error if err != nil { return err } - key := TradableBalanceKey(addr, denomT) - setDecimal(store, key, d) + key := ecocredit.TradableBalanceKey(addr, denomT) + ecocredit.SetDecimal(store, key, d) - key = TradableSupplyKey(denomT) - addAndSetDecimal(store, key, d) + key = ecocredit.TradableSupplyKey(denomT) + ecocredit.AddAndSetDecimal(store, key, d) } // set retired balance and update supply @@ -119,11 +119,11 @@ func setBalanceAndSupply(store sdk.KVStore, balances []*ecocredit.Balance) error if err != nil { return err } - key := RetiredBalanceKey(addr, denomT) - setDecimal(store, key, d) + key := ecocredit.RetiredBalanceKey(addr, denomT) + ecocredit.SetDecimal(store, key, d) - key = RetiredSupplyKey(denomT) - addAndSetDecimal(store, key, d) + key = ecocredit.RetiredSupplyKey(denomT) + ecocredit.AddAndSetDecimal(store, key, d) } } @@ -153,7 +153,7 @@ func (s serverImpl) ExportGenesis(ctx types.Context, cdc codec.Codec) (json.RawM } suppliesMap := make(map[string]*ecocredit.Supply) - iterateSupplies(store, TradableSupplyPrefix, func(denom, supply string) (bool, error) { + ecocredit.IterateSupplies(store, ecocredit.TradableSupplyPrefix, func(denom, supply string) (bool, error) { suppliesMap[denom] = &ecocredit.Supply{ BatchDenom: denom, TradableSupply: supply, @@ -162,7 +162,7 @@ func (s serverImpl) ExportGenesis(ctx types.Context, cdc codec.Codec) (json.RawM return false, nil }) - iterateSupplies(store, RetiredSupplyPrefix, func(denom, supply string) (bool, error) { + ecocredit.IterateSupplies(store, ecocredit.RetiredSupplyPrefix, func(denom, supply string) (bool, error) { if _, exists := suppliesMap[denom]; exists { suppliesMap[denom].RetiredSupply = supply } else { @@ -183,7 +183,7 @@ func (s serverImpl) ExportGenesis(ctx types.Context, cdc codec.Codec) (json.RawM } balancesMap := make(map[string]*ecocredit.Balance) - iterateBalances(store, TradableBalancePrefix, func(address, denom, balance string) bool { + ecocredit.IterateBalances(store, ecocredit.TradableBalancePrefix, func(address, denom, balance string) bool { balancesMap[fmt.Sprintf("%s%s", address, denom)] = &ecocredit.Balance{ Address: address, BatchDenom: denom, @@ -193,7 +193,7 @@ func (s serverImpl) ExportGenesis(ctx types.Context, cdc codec.Codec) (json.RawM return false }) - iterateBalances(store, RetiredBalancePrefix, func(address, denom, balance string) bool { + ecocredit.IterateBalances(store, ecocredit.RetiredBalancePrefix, func(address, denom, balance string) bool { index := fmt.Sprintf("%s%s", address, denom) if _, exists := balancesMap[index]; exists { balancesMap[index].RetiredBalance = balance diff --git a/x/ecocredit/server/helpers.go b/x/ecocredit/server/helpers.go deleted file mode 100644 index 20c8570bea..0000000000 --- a/x/ecocredit/server/helpers.go +++ /dev/null @@ -1,98 +0,0 @@ -package server - -import ( - "fmt" - - sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - - "github.com/regen-network/regen-ledger/types/math" - "github.com/regen-network/regen-ledger/x/ecocredit" -) - -func getDecimal(store sdk.KVStore, key []byte) (math.Dec, error) { - bz := store.Get(key) - if bz == nil { - return math.NewDecFromInt64(0), nil - } - - value, err := math.NewDecFromString(string(bz)) - if err != nil { - return math.Dec{}, sdkerrors.Wrap(err, fmt.Sprintf("can't unmarshal %s as decimal", bz)) - } - - return value, nil -} - -func setDecimal(store sdk.KVStore, key []byte, value math.Dec) { - // always remove all trailing zeros for canonical representation - value, _ = value.Reduce() - - if value.IsZero() { - store.Delete(key) - } else { - // use floating notation here always for canonical representation - store.Set(key, []byte(value.String())) - } -} - -func addAndSetDecimal(store sdk.KVStore, key []byte, x math.Dec) error { - value, err := getDecimal(store, key) - if err != nil { - return err - } - - value, err = value.Add(x) - if err != nil { - return err - } - - setDecimal(store, key, value) - return nil -} - -func subAndSetDecimal(store sdk.KVStore, key []byte, x math.Dec) error { - value, err := getDecimal(store, key) - if err != nil { - return err - } - - if value.Cmp(x) == -1 { - return ecocredit.ErrInsufficientFunds - } - - value, err = math.SafeSubBalance(value, x) - if err != nil { - return err - } - - setDecimal(store, key, value) - return nil -} - -func iterateSupplies(store sdk.KVStore, storeKey byte, cb func(denom, supply string) (bool, error)) error { - iter := sdk.KVStorePrefixIterator(store, []byte{storeKey}) - defer iter.Close() - for ; iter.Valid(); iter.Next() { - stop, err := cb(string(ParseSupplyKey(iter.Key())), string(iter.Value())) - if err != nil { - return err - } - if stop { - break - } - } - - return nil -} - -func iterateBalances(store sdk.KVStore, storeKey byte, cb func(address, denom, balance string) bool) { - iter := sdk.KVStorePrefixIterator(store, []byte{storeKey}) - defer iter.Close() - for ; iter.Valid(); iter.Next() { - addr, denom := ParseBalanceKey(iter.Key()) - if cb(addr.String(), string(denom), string(iter.Value())) { - break - } - } -} diff --git a/x/ecocredit/server/invariants.go b/x/ecocredit/server/invariants.go index fa03dc80ca..a7e02b664e 100644 --- a/x/ecocredit/server/invariants.go +++ b/x/ecocredit/server/invariants.go @@ -1,12 +1,13 @@ package server import ( + "context" "fmt" - "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/regen-network/regen-ledger/types/math" "github.com/regen-network/regen-ledger/x/ecocredit" + baskettypes "github.com/regen-network/regen-ledger/x/ecocredit/basket" ) // RegisterInvariants registers the ecocredit module invariants. @@ -18,18 +19,50 @@ func (s serverImpl) RegisterInvariants(ir sdk.InvariantRegistry) { func (s serverImpl) tradableSupplyInvariant() sdk.Invariant { return func(ctx sdk.Context) (string, bool) { store := ctx.KVStore(s.storeKey) - return tradableSupplyInvariant(store) + goCtx := sdk.WrapSDKContext(ctx) + basketBalances := s.getBasketBalanceMap(goCtx) + return tradableSupplyInvariant(store, basketBalances) } } -func tradableSupplyInvariant(store types.KVStore) (string, bool) { +func (s serverImpl) getBasketBalanceMap(ctx context.Context) map[string]math.Dec { + res, err := s.basketKeeper.Baskets(ctx, &baskettypes.QueryBasketsRequest{}) + if err != nil { + panic(err) + } + basketBalances := make(map[string]math.Dec) // map of batch_denom to balance + for _, basket := range res.Baskets { + res, err := s.basketKeeper.BasketBalances(ctx, &baskettypes.QueryBasketBalancesRequest{BasketDenom: basket.BasketDenom}) + if err != nil { + panic(err) + } + for _, bal := range res.Balances { + amount, err := math.NewDecFromString(bal.Balance) + if err != nil { + panic(err) + } + if existingBal, ok := basketBalances[bal.BatchDenom]; ok { + existingBal, err = existingBal.Add(amount) + if err != nil { + panic(err) + } + basketBalances[bal.BatchDenom] = existingBal + } else { + basketBalances[bal.BatchDenom] = amount + } + } + } + return basketBalances +} + +func tradableSupplyInvariant(store types.KVStore, basketBalances map[string]math.Dec) (string, bool) { var ( msg string broken bool ) calTradableSupplies := make(map[string]math.Dec) - iterateBalances(store, TradableBalancePrefix, func(_, denom, b string) bool { + ecocredit.IterateBalances(store, ecocredit.TradableBalancePrefix, func(_, denom, b string) bool { balance, err := math.NewNonNegativeDecFromString(b) if err != nil { broken = true @@ -49,7 +82,19 @@ func tradableSupplyInvariant(store types.KVStore) (string, bool) { return false }) - if err := iterateSupplies(store, TradableSupplyPrefix, func(denom string, s string) (bool, error) { + for denom, amt := range basketBalances { + if amount, ok := calTradableSupplies[denom]; ok { + amount, err := math.SafeAddBalance(amount, amt) + if err != nil { + panic(err) + } + calTradableSupplies[denom] = amount + } else { + panic("unknown denom in basket") + } + } + + if err := ecocredit.IterateSupplies(store, ecocredit.TradableSupplyPrefix, func(denom string, s string) (bool, error) { supply, err := math.NewNonNegativeDecFromString(s) if err != nil { broken = true @@ -85,7 +130,7 @@ func retiredSupplyInvariant(store types.KVStore) (string, bool) { broken bool ) calRetiredSupplies := make(map[string]math.Dec) - iterateBalances(store, RetiredBalancePrefix, func(_, denom, b string) bool { + ecocredit.IterateBalances(store, ecocredit.RetiredBalancePrefix, func(_, denom, b string) bool { balance, err := math.NewNonNegativeDecFromString(b) if err != nil { broken = true @@ -104,7 +149,7 @@ func retiredSupplyInvariant(store types.KVStore) (string, bool) { return false }) - if err := iterateSupplies(store, RetiredSupplyPrefix, func(denom, s string) (bool, error) { + if err := ecocredit.IterateSupplies(store, ecocredit.RetiredSupplyPrefix, func(denom, s string) (bool, error) { supply, err := math.NewNonNegativeDecFromString(s) if err != nil { broken = true diff --git a/x/ecocredit/server/invariants_test.go b/x/ecocredit/server/invariants_test.go index d31ce1260d..d01dac8d37 100644 --- a/x/ecocredit/server/invariants_test.go +++ b/x/ecocredit/server/invariants_test.go @@ -32,10 +32,11 @@ func TestTradableSupplyInvariants(t *testing.T) { acc2 := sdk.AccAddress([]byte("account2")) testCases := []struct { - msg string - balances []*ecocredit.Balance - supply []*ecocredit.Supply - expBroken bool + msg string + balances []*ecocredit.Balance + supply []*ecocredit.Supply + basketBalance map[string]math.Dec + expBroken bool }{ { "valid test case", @@ -59,10 +60,11 @@ func TestTradableSupplyInvariants(t *testing.T) { []*ecocredit.Supply{ { BatchDenom: "1/2", - TradableSupply: "310", + TradableSupply: "320", RetiredSupply: "210", }, }, + map[string]math.Dec{"1/2": math.NewDecFromInt64(10)}, false, }, { @@ -87,15 +89,16 @@ func TestTradableSupplyInvariants(t *testing.T) { []*ecocredit.Supply{ { BatchDenom: "1/2", - TradableSupply: "310.579", + TradableSupply: "320.579", RetiredSupply: "0", }, { BatchDenom: "3/4", - TradableSupply: "210.456", + TradableSupply: "220.456", RetiredSupply: "0", }, }, + map[string]math.Dec{"1/2": math.NewDecFromInt64(10), "3/4": math.NewDecFromInt64(10)}, false, }, { @@ -124,6 +127,7 @@ func TestTradableSupplyInvariants(t *testing.T) { RetiredSupply: "0", }, }, + map[string]math.Dec{}, true, }, { @@ -148,7 +152,7 @@ func TestTradableSupplyInvariants(t *testing.T) { []*ecocredit.Supply{ { BatchDenom: "1/2", - TradableSupply: "310.57", + TradableSupply: "325.57", RetiredSupply: "0", }, { @@ -157,6 +161,7 @@ func TestTradableSupplyInvariants(t *testing.T) { RetiredSupply: "0", }, }, + map[string]math.Dec{}, true, }, } @@ -171,7 +176,7 @@ func TestTradableSupplyInvariants(t *testing.T) { initSupply(t, store, tc.supply) - msg, broken := tradableSupplyInvariant(store) + msg, broken := tradableSupplyInvariant(store, tc.basketBalance) if tc.expBroken { require.True(t, broken, msg) } else { @@ -335,34 +340,34 @@ func TestRetiredSupplyInvariants(t *testing.T) { func initBalances(t *testing.T, store sdk.KVStore, balances []*ecocredit.Balance) { for _, b := range balances { - denomT := batchDenomT(b.BatchDenom) + denomT := ecocredit.BatchDenomT(b.BatchDenom) addr, err := sdk.AccAddressFromBech32(b.Address) require.NoError(t, err) if b.TradableBalance != "" { d, err := math.NewNonNegativeDecFromString(b.TradableBalance) require.NoError(t, err) - key := TradableBalanceKey(addr, denomT) - setDecimal(store, key, d) + key := ecocredit.TradableBalanceKey(addr, denomT) + ecocredit.SetDecimal(store, key, d) } if b.RetiredBalance != "" { d, err := math.NewNonNegativeDecFromString(b.RetiredBalance) require.NoError(t, err) - key := RetiredBalanceKey(addr, denomT) - setDecimal(store, key, d) + key := ecocredit.RetiredBalanceKey(addr, denomT) + ecocredit.SetDecimal(store, key, d) } } } func initSupply(t *testing.T, store sdk.KVStore, supply []*ecocredit.Supply) { for _, s := range supply { - denomT := batchDenomT(s.BatchDenom) + denomT := ecocredit.BatchDenomT(s.BatchDenom) d, err := math.NewNonNegativeDecFromString(s.TradableSupply) require.NoError(t, err) - key := TradableSupplyKey(denomT) - addAndSetDecimal(store, key, d) + key := ecocredit.TradableSupplyKey(denomT) + ecocredit.AddAndSetDecimal(store, key, d) d, err = math.NewNonNegativeDecFromString(s.RetiredSupply) require.NoError(t, err) - key = RetiredSupplyKey(denomT) - addAndSetDecimal(store, key, d) + key = ecocredit.RetiredSupplyKey(denomT) + ecocredit.AddAndSetDecimal(store, key, d) } } diff --git a/x/ecocredit/server/msg_server.go b/x/ecocredit/server/msg_server.go index d37bbcf822..19b64f734a 100644 --- a/x/ecocredit/server/msg_server.go +++ b/x/ecocredit/server/msg_server.go @@ -100,7 +100,7 @@ func (s serverImpl) CreateBatch(goCtx context.Context, req *ecocredit.MsgCreateB return nil, sdkerrors.ErrInvalidRequest.Wrap(err.Error()) } - batchDenom := batchDenomT(batchDenomStr) + batchDenom := ecocredit.BatchDenomT(batchDenomStr) tradableSupply := math.NewDecFromInt64(0) retiredSupply := math.NewDecFromInt64(0) @@ -148,7 +148,7 @@ func (s serverImpl) CreateBatch(goCtx context.Context, req *ecocredit.MsgCreateB return nil, err } - err = addAndSetDecimal(store, TradableBalanceKey(recipientAddr, batchDenom), tradable) + err = ecocredit.AddAndSetDecimal(store, ecocredit.TradableBalanceKey(recipientAddr, batchDenom), tradable) if err != nil { return nil, err } @@ -179,8 +179,8 @@ func (s serverImpl) CreateBatch(goCtx context.Context, req *ecocredit.MsgCreateB ctx.GasMeter().ConsumeGas(gasCostPerIteration, "batch issuance") } - setDecimal(store, TradableSupplyKey(batchDenom), tradableSupply) - setDecimal(store, RetiredSupplyKey(batchDenom), retiredSupply) + ecocredit.SetDecimal(store, ecocredit.TradableSupplyKey(batchDenom), tradableSupply) + ecocredit.SetDecimal(store, ecocredit.RetiredSupplyKey(batchDenom), retiredSupply) totalSupply, err := tradableSupply.Add(retiredSupply) if err != nil { @@ -240,7 +240,7 @@ func (s serverImpl) Send(goCtx context.Context, req *ecocredit.MsgSend) (*ecocre } for _, credit := range req.Credits { - denom := batchDenomT(credit.BatchDenom) + denom := ecocredit.BatchDenomT(credit.BatchDenom) if !s.batchInfoTable.Has(ctx, orm.RowID(denom)) { return nil, sdkerrors.ErrInvalidRequest.Wrapf("%s is not a valid credit batch denom", denom) } @@ -266,20 +266,20 @@ func (s serverImpl) Send(goCtx context.Context, req *ecocredit.MsgSend) (*ecocre } // subtract balance - err = subAndSetDecimal(store, TradableBalanceKey(senderAddr, denom), sum) + err = ecocredit.SubAndSetDecimal(store, ecocredit.TradableBalanceKey(senderAddr, denom), sum) if err != nil { return nil, err } // Add tradable balance - err = addAndSetDecimal(store, TradableBalanceKey(recipientAddr, denom), tradable) + err = ecocredit.AddAndSetDecimal(store, ecocredit.TradableBalanceKey(recipientAddr, denom), tradable) if err != nil { return nil, err } if !retired.IsZero() { // subtract retired from tradable supply - err = subAndSetDecimal(store, TradableSupplyKey(denom), retired) + err = ecocredit.SubAndSetDecimal(store, ecocredit.TradableSupplyKey(denom), retired) if err != nil { return nil, err } @@ -291,7 +291,7 @@ func (s serverImpl) Send(goCtx context.Context, req *ecocredit.MsgSend) (*ecocre } // Add retired supply - err = addAndSetDecimal(store, RetiredSupplyKey(denom), retired) + err = ecocredit.AddAndSetDecimal(store, ecocredit.RetiredSupplyKey(denom), retired) if err != nil { return nil, err } @@ -325,7 +325,7 @@ func (s serverImpl) Retire(goCtx context.Context, req *ecocredit.MsgRetire) (*ec } for _, credit := range req.Credits { - denom := batchDenomT(credit.BatchDenom) + denom := ecocredit.BatchDenomT(credit.BatchDenom) if !s.batchInfoTable.Has(ctx, orm.RowID(denom)) { return nil, sdkerrors.ErrInvalidRequest.Wrapf("%s is not a valid credit batch denom", denom) } @@ -352,7 +352,7 @@ func (s serverImpl) Retire(goCtx context.Context, req *ecocredit.MsgRetire) (*ec } // Add retired supply - err = addAndSetDecimal(store, RetiredSupplyKey(denom), toRetire) + err = ecocredit.AddAndSetDecimal(store, ecocredit.RetiredSupplyKey(denom), toRetire) if err != nil { return nil, err } @@ -376,7 +376,7 @@ func (s serverImpl) Cancel(goCtx context.Context, req *ecocredit.MsgCancel) (*ec // Check that the batch that were trying to cancel credits from // exists - denom := batchDenomT(credit.BatchDenom) + denom := ecocredit.BatchDenomT(credit.BatchDenom) if !s.batchInfoTable.Has(ctx, orm.RowID(denom)) { return nil, sdkerrors.ErrInvalidRequest.Wrapf("%s is not a valid credit batch denom", denom) } @@ -519,8 +519,8 @@ func (s serverImpl) nextBatchInClass(ctx types.Context, classInfo *ecocredit.Cla return nextVal, nil } -func retire(ctx types.Context, store sdk.KVStore, recipient sdk.AccAddress, batchDenom batchDenomT, retired math.Dec, location string) error { - err := addAndSetDecimal(store, RetiredBalanceKey(recipient, batchDenom), retired) +func retire(ctx types.Context, store sdk.KVStore, recipient sdk.AccAddress, batchDenom ecocredit.BatchDenomT, retired math.Dec, location string) error { + err := ecocredit.AddAndSetDecimal(store, ecocredit.RetiredBalanceKey(recipient, batchDenom), retired) if err != nil { return err } @@ -534,15 +534,15 @@ func retire(ctx types.Context, store sdk.KVStore, recipient sdk.AccAddress, batc } // subtracts `amount` from the tradable balance and tradable supply -func subtractTradableBalanceAndSupply(store sdk.KVStore, holder sdk.AccAddress, batchDenom batchDenomT, amount math.Dec) error { +func subtractTradableBalanceAndSupply(store sdk.KVStore, holder sdk.AccAddress, batchDenom ecocredit.BatchDenomT, amount math.Dec) error { // subtract tradable balance - err := subAndSetDecimal(store, TradableBalanceKey(holder, batchDenom), amount) + err := ecocredit.SubAndSetDecimal(store, ecocredit.TradableBalanceKey(holder, batchDenom), amount) if err != nil { return err } // subtract tradable supply - err = subAndSetDecimal(store, TradableSupplyKey(batchDenom), amount) + err = ecocredit.SubAndSetDecimal(store, ecocredit.TradableSupplyKey(batchDenom), amount) if err != nil { return err } @@ -551,7 +551,7 @@ func subtractTradableBalanceAndSupply(store sdk.KVStore, holder sdk.AccAddress, } // gets the precision of the credit type associated with the batch -func (s serverImpl) getBatchPrecision(ctx types.Context, denom batchDenomT) (uint32, error) { +func (s serverImpl) getBatchPrecision(ctx types.Context, denom ecocredit.BatchDenomT) (uint32, error) { var batchInfo ecocredit.BatchInfo err := s.batchInfoTable.GetOne(ctx, orm.RowID(denom), &batchInfo) if err != nil { diff --git a/x/ecocredit/server/query_server.go b/x/ecocredit/server/query_server.go index 8a78729948..3517a840aa 100644 --- a/x/ecocredit/server/query_server.go +++ b/x/ecocredit/server/query_server.go @@ -119,19 +119,19 @@ func (s serverImpl) Balance(goCtx context.Context, request *ecocredit.QueryBalan ctx := types.UnwrapSDKContext(goCtx) acc := request.Account - denom := batchDenomT(request.BatchDenom) + denom := ecocredit.BatchDenomT(request.BatchDenom) store := ctx.KVStore(s.storeKey) accAddr, err := sdk.AccAddressFromBech32(acc) if err != nil { return nil, err } - tradable, err := getDecimal(store, TradableBalanceKey(accAddr, denom)) + tradable, err := ecocredit.GetDecimal(store, ecocredit.TradableBalanceKey(accAddr, denom)) if err != nil { return nil, err } - retired, err := getDecimal(store, RetiredBalanceKey(accAddr, denom)) + retired, err := ecocredit.GetDecimal(store, ecocredit.RetiredBalanceKey(accAddr, denom)) if err != nil { return nil, err } @@ -154,14 +154,14 @@ func (s serverImpl) Supply(goCtx context.Context, request *ecocredit.QuerySupply ctx := types.UnwrapSDKContext(goCtx) store := ctx.KVStore(s.storeKey) - denom := batchDenomT(request.BatchDenom) + denom := ecocredit.BatchDenomT(request.BatchDenom) - tradable, err := getDecimal(store, TradableSupplyKey(denom)) + tradable, err := ecocredit.GetDecimal(store, ecocredit.TradableSupplyKey(denom)) if err != nil { return nil, err } - retired, err := getDecimal(store, RetiredSupplyKey(denom)) + retired, err := ecocredit.GetDecimal(store, ecocredit.RetiredSupplyKey(denom)) if err != nil { return nil, err } diff --git a/x/ecocredit/server/server.go b/x/ecocredit/server/server.go index 29c4f2519c..c64011a94a 100644 --- a/x/ecocredit/server/server.go +++ b/x/ecocredit/server/server.go @@ -15,22 +15,11 @@ import ( "github.com/regen-network/regen-ledger/x/ecocredit" ) -const ( - TradableBalancePrefix byte = 0x0 - TradableSupplyPrefix byte = 0x1 - RetiredBalancePrefix byte = 0x2 - RetiredSupplyPrefix byte = 0x3 - CreditTypeSeqTablePrefix byte = 0x4 - ClassInfoTablePrefix byte = 0x5 - BatchInfoTablePrefix byte = 0x6 - ORMPrefix byte = 0x7 -) - var ModuleSchema = ormdb.ModuleSchema{ FileDescriptors: map[uint32]protoreflect.FileDescriptor{ 1: basketv1.File_regen_ecocredit_basket_v1_state_proto, }, - Prefix: []byte{ORMPrefix}, + Prefix: []byte{ecocredit.ORMPrefix}, } type serverImpl struct { @@ -45,6 +34,8 @@ type serverImpl struct { classInfoTable orm.PrimaryKeyTable batchInfoTable orm.PrimaryKeyTable + + basketKeeper basket.Keeper } func newServer(storeKey sdk.StoreKey, paramSpace paramtypes.Subspace, @@ -56,19 +47,19 @@ func newServer(storeKey sdk.StoreKey, paramSpace paramtypes.Subspace, accountKeeper: accountKeeper, } - creditTypeSeqTable, err := orm.NewPrimaryKeyTableBuilder(CreditTypeSeqTablePrefix, storeKey, &ecocredit.CreditTypeSeq{}, cdc) + creditTypeSeqTable, err := orm.NewPrimaryKeyTableBuilder(ecocredit.CreditTypeSeqTablePrefix, storeKey, &ecocredit.CreditTypeSeq{}, cdc) if err != nil { panic(err.Error()) } s.creditTypeSeqTable = creditTypeSeqTable.Build() - classInfoTableBuilder, err := orm.NewPrimaryKeyTableBuilder(ClassInfoTablePrefix, storeKey, &ecocredit.ClassInfo{}, cdc) + classInfoTableBuilder, err := orm.NewPrimaryKeyTableBuilder(ecocredit.ClassInfoTablePrefix, storeKey, &ecocredit.ClassInfo{}, cdc) if err != nil { panic(err.Error()) } s.classInfoTable = classInfoTableBuilder.Build() - batchInfoTableBuilder, err := orm.NewPrimaryKeyTableBuilder(BatchInfoTablePrefix, storeKey, &ecocredit.BatchInfo{}, cdc) + batchInfoTableBuilder, err := orm.NewPrimaryKeyTableBuilder(ecocredit.BatchInfoTablePrefix, storeKey, &ecocredit.BatchInfo{}, cdc) if err != nil { panic(err.Error()) } @@ -80,17 +71,16 @@ func newServer(storeKey sdk.StoreKey, paramSpace paramtypes.Subspace, func RegisterServices(configurator server.Configurator, paramSpace paramtypes.Subspace, accountKeeper ecocredit.AccountKeeper, bankKeeper ecocredit.BankKeeper) { impl := newServer(configurator.ModuleKey(), paramSpace, accountKeeper, bankKeeper, configurator.Marshaler()) + db, err := ormutil.NewStoreKeyDB(ModuleSchema, configurator.ModuleKey(), ormdb.ModuleDBOptions{}) + if err != nil { + panic(err) + } + impl.basketKeeper = basket.NewKeeper(db, impl, bankKeeper, impl.storeKey) ecocredit.RegisterMsgServer(configurator.MsgServer(), impl) ecocredit.RegisterQueryServer(configurator.QueryServer(), impl) configurator.RegisterGenesisHandlers(impl.InitGenesis, impl.ExportGenesis) configurator.RegisterWeightedOperationsHandler(impl.WeightedOperations) configurator.RegisterInvariantsHandler(impl.RegisterInvariants) - db, err := ormutil.NewStoreKeyDB(ModuleSchema, configurator.ModuleKey(), ormdb.ModuleDBOptions{}) - if err != nil { - panic(err) - } - - _ = basket.NewKeeper(db, impl, bankKeeper) // TODO Msg and Query server registration }