diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 0411e53f7d..55cf9ada74 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -170,10 +170,10 @@ jobs: uses: ./.github/actions/setup-ubuntu - name: Setup docker user run: | - DOCKER_USER=$(id -u) - DOCKER_GROUP=$(id -g) - echo "DOCKER_USER=$DOCKER_USER" >> $GITHUB_ENV - echo "DOCKER_GROUP=$DOCKER_GROUP" >> $GITHUB_ENV + DOCKER_IDU=$(id -u) + DOCKER_IDG=$(id -g) + echo "DOCKER_IDU=$DOCKER_IDU" >> $GITHUB_ENV + echo "DOCKER_IDG=$DOCKER_IDG" >> $GITHUB_ENV - name: configure variables run: | test_required=$(./script/upgrades.sh test-required ${{ github.ref }}) diff --git a/app/app.go b/app/app.go index 9d5af9848c..55873de283 100644 --- a/app/app.go +++ b/app/app.go @@ -133,6 +133,7 @@ func NewApp( app := &AkashApp{ BaseApp: bapp, App: &apptypes.App{ + Cdc: appCodec, Log: logger, }, aminoCdc: aminoCdc, diff --git a/app/types/app.go b/app/types/app.go index b07a4739bf..7f4633c13e 100644 --- a/app/types/app.go +++ b/app/types/app.go @@ -121,6 +121,7 @@ type AppKeepers struct { } type App struct { + Cdc codec.Codec Keepers AppKeepers Configurator module.Configurator MM *module.Manager @@ -192,6 +193,10 @@ func (app *App) GetMemKey(storeKey string) *storetypes.MemoryStoreKey { return app.memKeys[storeKey] } +func (app *App) GetCodec() codec.Codec { + return app.Cdc +} + // InitSpecialKeepers initiates special keepers (crisis appkeeper, upgradekeeper, params keeper) func (app *App) InitSpecialKeepers( cdc codec.Codec, diff --git a/go.mod b/go.mod index 199fa55d8c..7c939f7b32 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( google.golang.org/grpc v1.75.0 gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.2 - pkg.akt.dev/go v0.1.5 + pkg.akt.dev/go v0.1.6 pkg.akt.dev/go/cli v0.1.4 pkg.akt.dev/go/sdl v0.1.1 ) diff --git a/go.sum b/go.sum index 769287910c..db778c74fa 100644 --- a/go.sum +++ b/go.sum @@ -3284,8 +3284,8 @@ nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0= nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= pgregory.net/rapid v0.5.5 h1:jkgx1TjbQPD/feRoK+S/mXw9e1uj6WilpHrXJowi6oA= pgregory.net/rapid v0.5.5/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= -pkg.akt.dev/go v0.1.5 h1:UdhU70YOzfJzzd1mT6dpnK7/5RWwV7N1zr1HRNmqtaw= -pkg.akt.dev/go v0.1.5/go.mod h1:67LZ0QbZMoCipadLNIR8HzMFcL4A4My7h9aMo516SGk= +pkg.akt.dev/go v0.1.6 h1:3wkDfEMWwe4ziUfNq6wUxRFSgYsL/uYF/uZgVfdet/U= +pkg.akt.dev/go v0.1.6/go.mod h1:GUDt3iohVNbt8yW4P5Q0D05zoMs2NXaojF2ZBZgfWUQ= pkg.akt.dev/go/cli v0.1.4 h1:wFPegnPwimWHi0v5LN6AnWZnwtwpnD6mb7Dp1HSuzlw= pkg.akt.dev/go/cli v0.1.4/go.mod h1:ZLqHZcq+D/8a27WTPYhmfCm2iGbNicWV1AwOhdspJ4Y= pkg.akt.dev/go/sdl v0.1.1 h1:3CcAqWeKouFlvUSjQMktWLDqftOjn4cBX37TRFT7BRM= diff --git a/make/test-upgrade.mk b/make/test-upgrade.mk index 29fdad29de..f2e6f96d2e 100644 --- a/make/test-upgrade.mk +++ b/make/test-upgrade.mk @@ -21,7 +21,7 @@ UPGRADE_FROM := $(shell cat $(ROOT_DIR)/meta.json | jq -r --arg name GENESIS_BINARY_VERSION := $(shell cat $(ROOT_DIR)/meta.json | jq -r --arg name $(UPGRADE_TO) '.upgrades[$$name].from_binary' | tr -d '\n') UPGRADE_BINARY_VERSION ?= local -SNAPSHOT_SOURCE ?= sandbox1 +SNAPSHOT_SOURCE ?= mainnet ifeq ($(SNAPSHOT_SOURCE),mainnet) SNAPSHOT_NETWORK := akashnet-2 @@ -67,6 +67,7 @@ test: init $(GO_TEST) -run "^\QTestUpgrade\E$$" -tags e2e.upgrade -timeout 180m -v -args \ -cosmovisor=$(COSMOVISOR) \ -workdir=$(AP_RUN_DIR)/validators \ + -sourcesdir=$(AKASH_ROOT) \ -config=$(TEST_CONFIG) \ -upgrade-name=$(UPGRADE_TO) \ -upgrade-version="$(UPGRADE_BINARY_VERSION)" \ @@ -75,7 +76,7 @@ test: init .PHONY: test-reset test-reset: $(ROOT_DIR)/script/upgrades.sh --workdir=$(AP_RUN_DIR) --config="$(PWD)/config.json" --uto=$(UPGRADE_TO) --snapshot-url=$(SNAPSHOT_URL) --chain-meta=$(CHAIN_METADATA_URL) --max-validators=$(MAX_VALIDATORS) clean - #$(ROOT_DIR)/script/upgrades.sh --workdir=$(AP_RUN_DIR) --config="$(PWD)/config.json" --uto=$(UPGRADE_TO) --snapshot-url=$(SNAPSHOT_URL) --gbv=$(GENESIS_BINARY_VERSION) --chain-meta=$(CHAIN_METADATA_URL) bins + $(ROOT_DIR)/script/upgrades.sh --workdir=$(AP_RUN_DIR) --config="$(PWD)/config.json" --uto=$(UPGRADE_TO) --snapshot-url=$(SNAPSHOT_URL) --gbv=$(GENESIS_BINARY_VERSION) --chain-meta=$(CHAIN_METADATA_URL) bins $(ROOT_DIR)/script/upgrades.sh --workdir=$(AP_RUN_DIR) --config="$(PWD)/config.json" --uto=$(UPGRADE_TO) --snapshot-url=$(SNAPSHOT_URL) --chain-meta=$(CHAIN_METADATA_URL) keys $(ROOT_DIR)/script/upgrades.sh --workdir=$(AP_RUN_DIR) --config="$(PWD)/config.json" --state-config=$(STATE_CONFIG) --snapshot-url=$(SNAPSHOT_URL) --chain-meta=$(CHAIN_METADATA_URL) --max-validators=$(MAX_VALIDATORS) prepare-state diff --git a/meta.json b/meta.json index 5dc439ee52..d80c0038d2 100644 --- a/meta.json +++ b/meta.json @@ -44,6 +44,11 @@ "skipped": false, "from_binary": "v0.38.6-rc2", "from_version": "v0.38.0" + }, + "v1.1.0": { + "skipped": false, + "from_binary": "v1.0.4", + "from_version": "v1.0.0" } } } diff --git a/tests/upgrade/sdktypes.go b/tests/upgrade/sdktypes.go new file mode 100644 index 0000000000..990506122c --- /dev/null +++ b/tests/upgrade/sdktypes.go @@ -0,0 +1,56 @@ +package upgrade + +import ( + "encoding/json" + + upgradetypes "cosmossdk.io/x/upgrade/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// These files defines sdk specific types necessary to perform upgrade simulation. +// we're not using SDK generated types to prevent import of different types of cosmos sdk + +type nodeStatus struct { + SyncInfo struct { + LatestBlockHeight string `json:"latest_block_height"` + CatchingUp bool `json:"catching_up"` + } `json:"sync_info"` +} + +type votingParams struct { + VotingPeriod string `json:"voting_period"` +} + +type depositParams struct { + MinDeposit sdk.Coins `json:"min_deposit"` +} + +type govParams struct { + VotingParams votingParams `json:"voting_params"` + DepositParams depositParams `json:"deposit_params"` +} + +type proposalResp struct { + ID string `json:"id"` + Title string `json:"title"` +} + +type proposalsResp struct { + Proposals []proposalResp `json:"proposals"` +} + +type SoftwareUpgradeProposal struct { + Type string `json:"@type"` + Authority string `json:"authority"` + Plan upgradetypes.Plan `json:"plan"` +} + +type ProposalMsg struct { + // Msgs defines an array of sdk.Msgs proto-JSON-encoded as Anys. + Messages []json.RawMessage `json:"messages,omitempty"` + Metadata string `json:"metadata"` + Deposit string `json:"deposit"` + Title string `json:"title"` + Summary string `json:"summary"` + Expedited bool `json:"expedited"` +} diff --git a/tests/upgrade/test-cases.json b/tests/upgrade/test-cases.json index 9edaacc110..3d86cdca02 100644 --- a/tests/upgrade/test-cases.json +++ b/tests/upgrade/test-cases.json @@ -1,4 +1,10 @@ { + "v1.1.0": { + "modules": { + }, + "migrations": { + } + }, "v1.0.0": { "modules": { "added": [ @@ -15,7 +21,8 @@ { "from": "3", "to": "4" - }, { + }, + { "from": "4", "to": "5" } @@ -164,7 +171,8 @@ { "from": "2", "to": "3" - }, { + }, + { "from": "3", "to": "4" }, diff --git a/tests/upgrade/test-config.json b/tests/upgrade/test-config.json index c6f36a4220..a9ec58b481 100644 --- a/tests/upgrade/test-config.json +++ b/tests/upgrade/test-config.json @@ -1,8 +1,7 @@ { "chain-id": "localakash", "validators": [ - ".akash0", - ".akash1" + ".akash0" ], "work": { "home": ".akash0", diff --git a/tests/upgrade/types/types.go b/tests/upgrade/types/types.go index cfc20f4cd5..5c5741a057 100644 --- a/tests/upgrade/types/types.go +++ b/tests/upgrade/types/types.go @@ -11,6 +11,7 @@ import ( type TestParams struct { Home string Node string + SourceDir string ChainID string KeyringBackend string From string diff --git a/tests/upgrade/upgrade_test.go b/tests/upgrade/upgrade_test.go index 8ff059ab5e..8207f924fb 100644 --- a/tests/upgrade/upgrade_test.go +++ b/tests/upgrade/upgrade_test.go @@ -22,7 +22,7 @@ import ( "testing" "time" - sdkmath "cosmossdk.io/math" + upgradetypes "cosmossdk.io/x/upgrade/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/mod/semver" @@ -131,42 +131,11 @@ type postUpgradeTestDone struct{} type eventShutdown struct{} -type votingParams struct { - VotingPeriod string `json:"voting_period"` -} - -type depositParams struct { - MinDeposit sdk.Coins `json:"min_deposit"` -} - -type govParams struct { - VotingParams votingParams `json:"voting_params"` - DepositParams depositParams `json:"deposit_params"` -} - -type proposalResp struct { - ProposalID string `json:"proposal_id"` - Content struct { - Title string `json:"title"` - } `json:"content"` -} - -type proposalsResp struct { - Proposals []proposalResp `json:"proposals"` -} - type wdReq struct { event watchdogCtrl resp chan<- struct{} } -type nodeStatus struct { - SyncInfo struct { - LatestBlockHeight string `json:"latest_block_height"` - CatchingUp bool `json:"catching_up"` - } `json:"SyncInfo"` -} - type testMigration struct { From string `json:"from"` To string `json:"to"` @@ -238,6 +207,7 @@ type upgradeTest struct { cancel context.CancelFunc group *errgroup.Group cmdr *commander + cacheDir string upgradeName string upgradeInfo string postUpgradeParams uttypes.TestParams @@ -261,6 +231,7 @@ type nodeInitParams struct { var ( workdir = flag.String("workdir", "", "work directory") + sourcesdir = flag.String("sourcesdir", "", "sources directory") config = flag.String("config", "", "config file") cosmovisor = flag.String("cosmovisor", "", "path to cosmovisor") upgradeVersion = flag.String("upgrade-version", "local", "akash release to download. local if it is built locally") @@ -297,6 +268,7 @@ func TestUpgrade(t *testing.T) { t.Log("detecting arguments") require.NotEqual(t, "", *workdir, "empty workdir flag") + require.NotEqual(t, "", *sourcesdir, "empty sourcesdir flag") require.NotEqual(t, "", *config, "empty config flag") require.NotEqual(t, "", *upgradeVersion, "empty upgrade-version flag") require.NotEqual(t, "", *upgradeName, "empty upgrade-name flag") @@ -312,6 +284,7 @@ func TestUpgrade(t *testing.T) { require.True(t, info.IsDir(), "workdir flag is not a dir") *workdir = strings.TrimSuffix(*workdir, "/") + *sourcesdir = strings.TrimSuffix(*sourcesdir, "/") info, err = os.Stat(*cosmovisor) require.NoError(t, err) @@ -373,9 +346,14 @@ func TestUpgrade(t *testing.T) { postUpgradeParams := uttypes.TestParams{} + var upgradeCache string for idx, name := range cfg.Validators { homedir := fmt.Sprintf("%s/%s", *workdir, name) + if idx == 0 { + upgradeCache = homedir + } + genesisBin := fmt.Sprintf("%s/cosmovisor/genesis/bin/akash", homedir) info, err = os.Stat(genesisBin) @@ -417,6 +395,7 @@ func TestUpgrade(t *testing.T) { t.Logf("validator address: \"%s\"", addr.String()) postUpgradeParams.Home = homedir + postUpgradeParams.SourceDir = *sourcesdir postUpgradeParams.ChainID = cfg.ChainID postUpgradeParams.Node = "tcp://127.0.0.1:26657" postUpgradeParams.KeyringBackend = "test" @@ -516,6 +495,7 @@ func TestUpgrade(t *testing.T) { "AKASH_GRPC_ENABLE=true", "AKASH_GRPC_WEB_ENABLE=true", "AKASH_API_ENABLE=true", + "AKASH_PRUNING=nothing", }, } } @@ -533,6 +513,7 @@ func TestUpgrade(t *testing.T) { ctx: ctx, group: group, cmdr: cmdr, + cacheDir: upgradeCache, upgradeName: *upgradeName, upgradeInfo: upgradeInfo, postUpgradeParams: postUpgradeParams, @@ -658,6 +639,18 @@ loop: return err } +type baseAccount struct { + Address string `json:"address"` +} + +type moduleAccount struct { + BaseAccount baseAccount `json:"base_account"` +} + +type accountResp struct { + Account moduleAccount `json:"account"` +} + func (l *upgradeTest) submitUpgradeProposal() error { var err error @@ -688,17 +681,29 @@ func (l *upgradeTest) submitUpgradeProposal() error { } } - tm := time.NewTimer(30 * time.Second) - select { - case <-l.ctx.Done(): - if !tm.Stop() { - <-tm.C - } - err = l.ctx.Err() + cmdRes, err = l.cmdr.execute(l.ctx, "query auth module-account gov") + if err != nil { + l.t.Logf("executing cmd failed: %s\n", string(cmdRes)) return err - case <-tm.C: } + macc := accountResp{} + err = json.Unmarshal(cmdRes, &macc) + if err != nil { + return err + } + + //tm := time.NewTimer(30 * time.Second) + //select { + //case <-l.ctx.Done(): + // if !tm.Stop() { + // <-tm.C + // } + // err = l.ctx.Err() + // return err + //case <-tm.C: + //} + cmdRes, err = l.cmdr.execute(l.ctx, "query gov params") if err != nil { l.t.Logf("executing cmd failed: %s\n", string(cmdRes)) @@ -712,13 +717,11 @@ func (l *upgradeTest) submitUpgradeProposal() error { return err } - votePeriod, valid := sdkmath.NewIntFromString(params.VotingParams.VotingPeriod) - if !valid { + votePeriod, err := time.ParseDuration(params.VotingParams.VotingPeriod) + if err != nil { return fmt.Errorf("invalid vote period value (%s)", params.VotingParams.VotingPeriod) } - votePeriod = votePeriod.QuoRaw(1e9) - cmdRes, err = l.cmdr.execute(l.ctx, "status") if err != nil { l.t.Logf("executing cmd failed: %s\n", string(cmdRes)) @@ -730,27 +733,55 @@ func (l *upgradeTest) submitUpgradeProposal() error { return err } - upgradeHeight, err := strconv.ParseUint(statusResp.SyncInfo.LatestBlockHeight, 10, 64) + upgradeHeight, err := strconv.ParseInt(statusResp.SyncInfo.LatestBlockHeight, 10, 64) + if err != nil { + return err + } + + upgradeHeight += int64(votePeriod/(6*time.Second)) + 10 + + upgradeProp := SoftwareUpgradeProposal{ + Type: "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade", + Authority: macc.Account.BaseAccount.Address, + Plan: upgradetypes.Plan{ + Name: l.upgradeName, + Height: upgradeHeight, + Info: l.upgradeInfo, + }, + } + + jup, err := json.Marshal(&upgradeProp) + if err != nil { + return err + } + + prop := &ProposalMsg{ + Messages: []json.RawMessage{ + jup, + }, + Deposit: params.DepositParams.MinDeposit[0].String(), + Title: l.upgradeName, + Summary: l.upgradeName, + Expedited: false, + } + + jProp, err := json.Marshal(prop) if err != nil { return err } - upgradeHeight += (votePeriod.Uint64() / 6) + 10 + propFile := fmt.Sprintf("%s/upgrade-prop-%s.json", l.cacheDir, l.upgradeName) + err = os.WriteFile(propFile, jProp, 0644) + if err != nil { + return err + } - l.t.Logf("voting period: %ss, curr height: %s, upgrade height: %d", + l.t.Logf("voting period: %s, curr height: %s, upgrade height: %d", votePeriod, statusResp.SyncInfo.LatestBlockHeight, upgradeHeight) - cmd := fmt.Sprintf(`tx gov submit-proposal software-upgrade %s --title=%[1]s --description="%[1]s" --upgrade-height=%d --deposit=%s`, - l.upgradeName, - upgradeHeight, - params.DepositParams.MinDeposit[0].String(), - ) - - if l.upgradeInfo != "" { - cmd += fmt.Sprintf(` --upgrade-info='%s'`, l.upgradeInfo) - } + cmd := fmt.Sprintf(`tx gov submit-proposal %s`, propFile) cmdRes, err = l.cmdr.execute(l.ctx, cmd) if err != nil { @@ -758,8 +789,8 @@ func (l *upgradeTest) submitUpgradeProposal() error { return err } - // give it two blocks to make sure proposal has been commited - tmctx, cancel := context.WithTimeout(l.ctx, 12*time.Second) + // give it two blocks to make sure a proposal has been commited + tmctx, cancel := context.WithTimeout(l.ctx, 18*time.Second) defer cancel() <-tmctx.Done() @@ -784,8 +815,8 @@ func (l *upgradeTest) submitUpgradeProposal() error { var propID string for i := len(proposals.Proposals) - 1; i >= 0; i-- { - if proposals.Proposals[i].Content.Title == l.upgradeName { - propID = proposals.Proposals[i].ProposalID + if proposals.Proposals[i].Title == l.upgradeName { + propID = proposals.Proposals[i].ID break } } diff --git a/tests/upgrade/workers_test.go b/tests/upgrade/workers_test.go index 171802ba3d..e9e93e4ea0 100644 --- a/tests/upgrade/workers_test.go +++ b/tests/upgrade/workers_test.go @@ -6,139 +6,17 @@ import ( "context" "testing" - "github.com/stretchr/testify/require" - - sdkmath "cosmossdk.io/math" - sdkclient "github.com/cosmos/cosmos-sdk/client" - sdk "github.com/cosmos/cosmos-sdk/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" - - "pkg.akt.dev/go/cli/flags" - arpcclient "pkg.akt.dev/go/node/client" - aclient "pkg.akt.dev/go/node/client/discovery" - cltypes "pkg.akt.dev/go/node/client/types" - "pkg.akt.dev/go/node/client/v1beta3" - "pkg.akt.dev/go/sdkutil" - - "pkg.akt.dev/node/app" uttypes "pkg.akt.dev/node/tests/upgrade/types" ) func init() { - uttypes.RegisterPostUpgradeWorker("v1.0.0", &postUpgrade{}) + uttypes.RegisterPostUpgradeWorker("v1.1.0", &postUpgrade{}) } -type postUpgrade struct { - cl v1beta3.Client -} +type postUpgrade struct{} var _ uttypes.TestWorker = (*postUpgrade)(nil) func (pu *postUpgrade) Run(ctx context.Context, t *testing.T, params uttypes.TestParams) { - encodingConfig := sdkutil.MakeEncodingConfig() - app.ModuleBasics().RegisterInterfaces(encodingConfig.InterfaceRegistry) - - rpcClient, err := arpcclient.NewClient(ctx, params.Node) - require.NoError(t, err) - - cctx := sdkclient.Context{}. - WithCodec(encodingConfig.Codec). - WithInterfaceRegistry(encodingConfig.InterfaceRegistry). - WithTxConfig(encodingConfig.TxConfig). - WithLegacyAmino(encodingConfig.Amino). - WithAccountRetriever(authtypes.AccountRetriever{}). - WithBroadcastMode(flags.BroadcastBlock). - WithHomeDir(params.Home). - WithChainID(params.ChainID). - WithNodeURI(params.Node). - WithClient(rpcClient). - WithSkipConfirmation(true). - WithFrom(params.From). - WithFromName(params.From). - WithFromAddress(params.FromAddress). - WithKeyringDir(params.Home). - WithSignModeStr(flags.SignModeDirect). - WithSimulation(false) - - kr, err := sdkclient.NewKeyringFromBackend(cctx, params.KeyringBackend) - require.NoError(t, err) - - cctx = cctx.WithKeyring(kr) - - opts := []cltypes.ClientOption{ - cltypes.WithGasPrices("0.025uakt"), - cltypes.WithGas(cltypes.GasSetting{Simulate: false, Gas: 1000000}), - cltypes.WithGasAdjustment(2), - } - - pu.cl, err = aclient.DiscoverClient(ctx, cctx, opts...) - require.NoError(t, err) - require.NotNil(t, pu.cl) - - pu.testGov(ctx, t) - - pu.testStaking(ctx, t) -} - -func (pu *postUpgrade) testGov(ctx context.Context, t *testing.T) { - t.Logf("testing gov module") - cctx := pu.cl.ClientContext() - - paramsResp, err := pu.cl.Query().Gov().Params(ctx, &govtypes.QueryParamsRequest{ParamsType: "deposit"}) - require.NoError(t, err) - require.NotNil(t, paramsResp) - - // paramsResp.Params.ExpeditedMinDeposit. - require.Equal(t, sdk.Coins{sdk.NewCoin("uakt", sdkmath.NewInt(2000000000))}.String(), sdk.Coins(paramsResp.Params.ExpeditedMinDeposit).String(), "ExpeditedMinDeposit must have 2000AKT") - require.Equal(t, paramsResp.Params.MinInitialDepositRatio, sdkmath.LegacyNewDecWithPrec(40, 2).String(), "MinInitialDepositRatio must be 40%") - - opAddr := sdk.ValAddress(cctx.FromAddress) - - comVal := sdkmath.LegacyNewDecWithPrec(4, 2) - - valResp, err := pu.cl.Query().Staking().Validator(ctx, &stakingtypes.QueryValidatorRequest{ValidatorAddr: opAddr.String()}) - require.NoError(t, err) - - minSelfDelegation := sdkmath.NewInt(1) - - tx := stakingtypes.NewMsgEditValidator(opAddr.String(), valResp.Validator.Description, &comVal, &minSelfDelegation) - broadcastResp, err := pu.cl.Tx().BroadcastMsgs(ctx, []sdk.Msg{tx}) - require.Error(t, err) - require.NotNil(t, broadcastResp) - - require.IsType(t, &sdk.TxResponse{}, broadcastResp) - txResp := broadcastResp.(*sdk.TxResponse) - require.NotEqual(t, uint32(0), txResp.Code, "update validator commission should fail if new value is < 5%") -} - -func (pu *postUpgrade) testStaking(ctx context.Context, t *testing.T) { - t.Logf("testing staking module") - - cctx := pu.cl.ClientContext() - - paramsResp, err := pu.cl.Query().Staking().Params(ctx, &stakingtypes.QueryParamsRequest{}) - require.NoError(t, err) - require.NotNil(t, paramsResp) - - require.True(t, paramsResp.Params.MinCommissionRate.GTE(sdkmath.LegacyNewDecWithPrec(5, 2)), "per upgrade v1.0.0 MinCommissionRate should be 5%") - - opAddr := sdk.ValAddress(cctx.FromAddress) - - comVal := sdkmath.LegacyNewDecWithPrec(4, 2) - - valResp, err := pu.cl.Query().Staking().Validator(ctx, &stakingtypes.QueryValidatorRequest{ValidatorAddr: opAddr.String()}) - require.NoError(t, err) - - minSelfDelegation := sdkmath.NewInt(1) - - tx := stakingtypes.NewMsgEditValidator(opAddr.String(), valResp.Validator.Description, &comVal, &minSelfDelegation) - broadcastResp, err := pu.cl.Tx().BroadcastMsgs(ctx, []sdk.Msg{tx}) - require.Error(t, err) - require.NotNil(t, broadcastResp) - require.IsType(t, &sdk.TxResponse{}, broadcastResp) - txResp := broadcastResp.(*sdk.TxResponse) - require.NotEqual(t, uint32(0), txResp.Code, "update validator commission should fail if new value is < 5%") } diff --git a/testutil/state/suite.go b/testutil/state/suite.go index f068097faa..cf063291eb 100644 --- a/testutil/state/suite.go +++ b/testutil/state/suite.go @@ -76,15 +76,8 @@ func SetupTestSuiteWithKeepers(t testing.TB, keepers Keepers) *TestSuite { if keepers.Bank == nil { bkeeper := &emocks.BankKeeper{} - bkeeper. - On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(nil) - bkeeper. - On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(nil) - bkeeper. - On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(nil) + // do not set bank mock during suite setup, each test must set them manually + // to make sure escrow balance values are tracked correctly bkeeper. On("SpendableCoin", mock.Anything, mock.Anything, mock.Anything). Return(sdk.NewInt64Coin("uakt", 10000000)) @@ -169,6 +162,10 @@ func SetupTestSuiteWithKeepers(t testing.TB, keepers Keepers) *TestSuite { } } +func (ts *TestSuite) PrepareMocks(fn func(ts *TestSuite)) { + fn(ts) +} + func (ts *TestSuite) App() *app.AkashApp { return ts.app } diff --git a/upgrades/software/v1.1.0/init.go b/upgrades/software/v1.1.0/init.go new file mode 100644 index 0000000000..4115a2760e --- /dev/null +++ b/upgrades/software/v1.1.0/init.go @@ -0,0 +1,11 @@ +// Package v1_1_0 +// nolint revive +package v1_1_0 + +import ( + utypes "pkg.akt.dev/node/upgrades/types" +) + +func init() { + utypes.RegisterUpgrade(UpgradeName, initUpgrade) +} diff --git a/upgrades/software/v1.1.0/upgrade.go b/upgrades/software/v1.1.0/upgrade.go new file mode 100644 index 0000000000..8e244037ad --- /dev/null +++ b/upgrades/software/v1.1.0/upgrade.go @@ -0,0 +1,466 @@ +// Package v1_1_0 +// nolint revive +package v1_1_0 + +import ( + "context" + "fmt" + + "cosmossdk.io/log" + sdkmath "cosmossdk.io/math" + "cosmossdk.io/store/prefix" + storetypes "cosmossdk.io/store/types" + upgradetypes "cosmossdk.io/x/upgrade/types" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + + dv1 "pkg.akt.dev/go/node/deployment/v1" + dtypes "pkg.akt.dev/go/node/deployment/v1beta4" + escrowid "pkg.akt.dev/go/node/escrow/id/v1" + idv1 "pkg.akt.dev/go/node/escrow/id/v1" + emodule "pkg.akt.dev/go/node/escrow/module" + etypes "pkg.akt.dev/go/node/escrow/types/v1" + mv1 "pkg.akt.dev/go/node/market/v1" + mtypes "pkg.akt.dev/go/node/market/v1beta5" + + apptypes "pkg.akt.dev/node/app/types" + utypes "pkg.akt.dev/node/upgrades/types" + ekeeper "pkg.akt.dev/node/x/escrow/keeper" + "pkg.akt.dev/node/x/market" + mhooks "pkg.akt.dev/node/x/market/hooks" + "pkg.akt.dev/node/x/market/keeper/keys" +) + +const ( + UpgradeName = "v1.1.0" +) + +type upgrade struct { + *apptypes.App + log log.Logger +} + +var _ utypes.IUpgrade = (*upgrade)(nil) + +func initUpgrade(log log.Logger, app *apptypes.App) (utypes.IUpgrade, error) { + up := &upgrade{ + App: app, + log: log.With("module", fmt.Sprintf("upgrade/%s", UpgradeName)), + } + + return up, nil +} + +func (up *upgrade) StoreLoader() *storetypes.StoreUpgrades { + return &storetypes.StoreUpgrades{} +} + +func (up *upgrade) UpgradeHandler() upgradetypes.UpgradeHandler { + return func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { + toVM, err := up.MM.RunMigrations(ctx, up.Configurator, fromVM) + if err != nil { + return nil, err + } + + sctx := sdk.UnwrapSDKContext(ctx) + err = up.closeOverdrawnEscrowAccounts(sctx) + if err != nil { + return nil, err + } + + up.log.Info(fmt.Sprintf("all migrations have been completed")) + + return toVM, err + } +} + +func (up *upgrade) closeOverdrawnEscrowAccounts(ctx sdk.Context) error { + store := ctx.KVStore(up.GetKey(emodule.StoreKey)) + searchPrefix := ekeeper.BuildSearchPrefix(ekeeper.AccountPrefix, etypes.StateOpen.String(), "") + + searchStore := prefix.NewStore(store, searchPrefix) + + iter := searchStore.Iterator(nil, nil) + defer func() { + _ = iter.Close() + }() + + cdc := up.GetCodec() + + totalAccounts := 0 + totalPayments := 0 + + for ; iter.Valid(); iter.Next() { + id, _ := ekeeper.ParseAccountKey(append(searchPrefix, iter.Key()...)) + val := etypes.Account{ + ID: id, + } + + cdc.MustUnmarshal(iter.Value(), &val.State) + + if val.State.Funds[0].Denom != "ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1" { + continue + } + + aPrevState := val.State.State + + heightDelta := ctx.BlockHeight() + val.State.SettledAt + + totalAvailableDeposits := sdkmath.LegacyZeroDec() + + for _, deposit := range val.State.Deposits { + totalAvailableDeposits.AddMut(deposit.Balance.Amount) + } + + payments := up.accountPayments(cdc, store, id, []etypes.State{etypes.StateOpen, etypes.StateOverdrawn}) + + totalBlockRate := sdkmath.LegacyZeroDec() + + for _, pmnt := range payments { + totalBlockRate.AddMut(pmnt.State.Rate.Amount) + + if pmnt.State.State == etypes.StateOverdrawn { + val.State.State = etypes.StateOverdrawn + } + } + + owed := sdkmath.LegacyZeroDec() + owed.AddMut(totalBlockRate) + owed.MulInt64Mut(heightDelta) + + overdraft := totalAvailableDeposits.LTE(owed) || val.State.State == etypes.StateOverdrawn + + totalAccounts++ + + val.State.Deposits = nil + val.State.Funds[0].Amount = val.State.Funds[0].Amount.Sub(owed) + + key := ekeeper.BuildAccountsKey(aPrevState, &val.ID) + store.Delete(key) + + if !overdraft { + val.State.State = etypes.StateClosed + } + + // find associated deployment/groups/lease/bid and close it + hooks := mhooks.New(up.Keepers.Akash.Deployment, up.Keepers.Akash.Market) + + err := up.OnEscrowAccountClosed(ctx, val) + if err != nil { + return err + } + + key = ekeeper.BuildAccountsKey(val.State.State, &val.ID) + store.Set(key, cdc.MustMarshal(&val.State)) + + for i := range payments { + totalPayments++ + key = ekeeper.BuildPaymentsKey(payments[i].State.State, &payments[i].ID) + store.Delete(key) + + payments[i].State.State = etypes.StateClosed + if overdraft { + payments[i].State.State = etypes.StateOverdrawn + } + + payments[i].State.Balance.Amount.Set(sdkmath.LegacyZeroDec()) + payments[i].State.Unsettled.Amount.Set(payments[i].State.Rate.Amount.MulInt64Mut(heightDelta)) + + key = ekeeper.BuildPaymentsKey(payments[i].State.State, &payments[i].ID) + err = hooks.OnEscrowPaymentClosed(ctx, payments[i]) + if err != nil { + return err + } + + store.Set(key, cdc.MustMarshal(&payments[i].State)) + } + } + + biter := searchStore.Iterator(nil, nil) + defer func() { + _ = biter.Close() + }() + + for ; biter.Valid(); biter.Next() { + eid, _ := ekeeper.ParseAccountKey(append(searchPrefix, biter.Key()...)) + val := etypes.Account{ + ID: eid, + } + + if eid.Scope != idv1.ScopeDeployment { + continue + } + + cdc.MustUnmarshal(biter.Value(), &val.State) + aPrevState := val.State.State + + did, err := dv1.DeploymentIDFromEscrowID(val.ID) + if err != nil { + return err + } + + deployment, found := up.Keepers.Akash.Deployment.GetDeployment(ctx, did) + if !found { + return nil + } + + if deployment.State == dv1.DeploymentClosed { + totalAccounts++ + + val.State.Deposits = nil + val.State.State = etypes.StateClosed + val.State.Funds[0].Amount.Set(sdkmath.LegacyZeroDec()) + + key := ekeeper.BuildAccountsKey(aPrevState, &val.ID) + store.Delete(key) + + key = ekeeper.BuildAccountsKey(val.State.State, &val.ID) + store.Set(key, cdc.MustMarshal(&val.State)) + + payments := up.accountPayments(cdc, store, eid, []etypes.State{etypes.StateOpen, etypes.StateOverdrawn}) + + for i := range payments { + totalPayments++ + key = ekeeper.BuildPaymentsKey(payments[i].State.State, &payments[i].ID) + store.Delete(key) + + payments[i].State.State = etypes.StateClosed + payments[i].State.Balance.Amount.Set(sdkmath.LegacyZeroDec()) + + key = ekeeper.BuildPaymentsKey(payments[i].State.State, &payments[i].ID) + store.Set(key, cdc.MustMarshal(&payments[i].State)) + } + } + } + + up.log.Info(fmt.Sprintf("cleaned up overdrawn:\n"+ + "\taccounts: %d\n"+ + "\tpayments: %d", totalAccounts, totalPayments)) + + return nil +} + +func (up *upgrade) accountPayments(cdc codec.Codec, store storetypes.KVStore, id escrowid.Account, states []etypes.State) []etypes.Payment { + var payments []etypes.Payment + + iters := make([]storetypes.Iterator, 0, len(states)) + defer func() { + for _, iter := range iters { + _ = iter.Close() + } + }() + + for _, state := range states { + pprefix := ekeeper.BuildPaymentsKey(state, &id) + iter := storetypes.KVStorePrefixIterator(store, pprefix) + iters = append(iters, iter) + + for ; iter.Valid(); iter.Next() { + id, _ := ekeeper.ParsePaymentKey(iter.Key()) + val := etypes.Payment{ + ID: id, + } + cdc.MustUnmarshal(iter.Value(), &val.State) + payments = append(payments, val) + } + } + return payments +} + +func (up *upgrade) OnEscrowAccountClosed(ctx sdk.Context, obj etypes.Account) error { + id, err := dv1.DeploymentIDFromEscrowID(obj.ID) + if err != nil { + return err + } + + deployment, found := up.Keepers.Akash.Deployment.GetDeployment(ctx, id) + if !found { + return nil + } + + if deployment.State != dv1.DeploymentActive { + return nil + } + err = up.Keepers.Akash.Deployment.CloseDeployment(ctx, deployment) + if err != nil { + return err + } + + gstate := dtypes.GroupClosed + if obj.State.State == etypes.StateOverdrawn { + gstate = dtypes.GroupInsufficientFunds + } + + for _, group := range up.Keepers.Akash.Deployment.GetGroups(ctx, deployment.ID) { + if group.ValidateClosable() == nil { + err = up.Keepers.Akash.Deployment.OnCloseGroup(ctx, group, gstate) + if err != nil { + return err + } + err = up.OnGroupClosed(ctx, group.ID) + if err != nil { + return err + } + } + } + + return nil +} + +func (up *upgrade) OnGroupClosed(ctx sdk.Context, id dv1.GroupID) error { + processClose := func(ctx sdk.Context, bid mtypes.Bid) error { + err := up.Keepers.Akash.Market.OnBidClosed(ctx, bid) + if err != nil { + return err + } + + if lease, ok := up.Keepers.Akash.Market.GetLease(ctx, bid.ID.LeaseID()); ok { + // OnGroupClosed is callable by x/deployment only so only reason is owner + err = up.Keepers.Akash.Market.OnLeaseClosed(ctx, lease, mv1.LeaseClosed, mv1.LeaseClosedReasonOwner) + if err != nil { + return err + } + } + + return nil + } + + var err error + up.Keepers.Akash.Market.WithOrdersForGroup(ctx, id, mtypes.OrderActive, func(order mtypes.Order) bool { + err = up.Keepers.Akash.Market.OnOrderClosed(ctx, order) + if err != nil { + return true + } + + up.Keepers.Akash.Market.WithBidsForOrder(ctx, order.ID, mtypes.BidOpen, func(bid mtypes.Bid) bool { + err = processClose(ctx, bid) + return err != nil + }) + + if err != nil { + return true + } + + up.Keepers.Akash.Market.WithBidsForOrder(ctx, order.ID, mtypes.BidActive, func(bid mtypes.Bid) bool { + err = processClose(ctx, bid) + return err != nil + }) + + return err != nil + }) + + if err != nil { + return err + } + + return nil +} + +func (up *upgrade) OnEscrowPaymentClosed(ctx sdk.Context, obj etypes.Payment) error { + id, err := mv1.LeaseIDFromPaymentID(obj.ID) + if err != nil { + return nil + } + + bid, ok := up.Keepers.Akash.Market.GetBid(ctx, id.BidID()) + if !ok { + return nil + } + + if bid.State != mtypes.BidActive { + return nil + } + + order, ok := up.Keepers.Akash.Market.GetOrder(ctx, id.OrderID()) + if !ok { + return mv1.ErrOrderNotFound + } + + lease, ok := up.Keepers.Akash.Market.GetLease(ctx, id) + if !ok { + return mv1.ErrLeaseNotFound + } + + err = up.Keepers.Akash.Market.OnOrderClosed(ctx, order) + if err != nil { + return err + } + err = up.OnBidClosed(ctx, bid) + if err != nil { + return err + } + + if obj.State.State == etypes.StateOverdrawn { + err = up.Keepers.Akash.Market.OnLeaseClosed(ctx, lease, mv1.LeaseInsufficientFunds, mv1.LeaseClosedReasonInsufficientFunds) + if err != nil { + return err + } + } else { + err = up.Keepers.Akash.Market.OnLeaseClosed(ctx, lease, mv1.LeaseClosed, mv1.LeaseClosedReasonUnspecified) + if err != nil { + return err + } + } + + return nil +} + +// OnBidClosed updates bid state to closed +func (up *upgrade) OnBidClosed(ctx sdk.Context, bid mtypes.Bid) error { + switch bid.State { + case mtypes.BidClosed, mtypes.BidLost: + return nil + } + + currState := bid.State + bid.State = mtypes.BidClosed + up.updateBid(ctx, bid, currState) + + err := ctx.EventManager().EmitTypedEvent( + &mv1.EventBidClosed{ + ID: bid.ID, + }, + ) + if err != nil { + return err + } + + return nil +} + +func (up *upgrade) updateBid(ctx sdk.Context, bid mtypes.Bid, currState mtypes.Bid_State) { + store := ctx.KVStore(up.GetKey(market.StoreKey)) + + switch currState { + case mtypes.BidOpen: + case mtypes.BidActive: + default: + panic(fmt.Sprintf("unexpected current state of the bid: %d", currState)) + } + + key := keys.MustBidKey(keys.BidStateToPrefix(currState), bid.ID) + revKey := keys.MustBidStateRevereKey(currState, bid.ID) + store.Delete(key) + if revKey != nil { + store.Delete(revKey) + } + + switch bid.State { + case mtypes.BidActive: + case mtypes.BidLost: + case mtypes.BidClosed: + default: + panic(fmt.Sprintf("unexpected new state of the bid: %d", bid.State)) + } + + data := up.App.Cdc.MustMarshal(&bid) + + key = keys.MustBidKey(keys.BidStateToPrefix(bid.State), bid.ID) + revKey = keys.MustBidStateRevereKey(bid.State, bid.ID) + + store.Set(key, data) + if len(revKey) > 0 { + store.Set(revKey, data) + } +} diff --git a/upgrades/upgrades.go b/upgrades/upgrades.go index ea143ded18..01852a09d7 100644 --- a/upgrades/upgrades.go +++ b/upgrades/upgrades.go @@ -2,5 +2,5 @@ package upgrades import ( // nolint: revive - _ "pkg.akt.dev/node/upgrades/software/v1.0.0" + _ "pkg.akt.dev/node/upgrades/software/v1.1.0" ) diff --git a/x/deployment/handler/handler_test.go b/x/deployment/handler/handler_test.go index f058b32f31..4752b53eba 100644 --- a/x/deployment/handler/handler_test.go +++ b/x/deployment/handler/handler_test.go @@ -7,21 +7,23 @@ import ( "testing" "time" - sdkmath "cosmossdk.io/math" - authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - mtypes "pkg.akt.dev/go/node/market/v1" + sdkmath "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/baseapp" sdktestdata "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" "pkg.akt.dev/go/node/deployment/v1" "pkg.akt.dev/go/node/deployment/v1beta4" + emodule "pkg.akt.dev/go/node/escrow/module" ev1 "pkg.akt.dev/go/node/escrow/v1" + mtypes "pkg.akt.dev/go/node/market/v1" deposit "pkg.akt.dev/go/node/types/deposit/v1" "pkg.akt.dev/go/testutil" @@ -112,15 +114,6 @@ func setupTestSuite(t *testing.T) *testSuite { bankKeeper. On("SpendableCoin", mock.Anything, mock.Anything, mock.Anything). Return(sdk.NewInt64Coin("uakt", 10000000)) - bankKeeper. - On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(nil) - bankKeeper. - On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(nil) - bankKeeper. - On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(nil) keepers := state.Keepers{ Authz: authzKeeper, @@ -147,7 +140,7 @@ func setupTestSuite(t *testing.T) *testSuite { return suite } -func TestProviderBadMessageType(t *testing.T) { +func TestHandlerBadMessageType(t *testing.T) { suite := setupTestSuite(t) res, err := suite.dhandler(suite.ctx, sdk.Msg(sdktestdata.NewTestMsg())) @@ -161,6 +154,8 @@ func TestCreateDeployment(t *testing.T) { deployment, groups := suite.createDeployment() + owner := sdk.MustAccAddressFromBech32(deployment.ID.Owner) + msg := &v1beta4.MsgCreateDeployment{ ID: deployment.ID, Groups: make(v1beta4.GroupSpecs, 0, len(groups)), @@ -174,6 +169,14 @@ func TestCreateDeployment(t *testing.T) { msg.Groups = append(msg.Groups, group.GroupSpec) } + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, owner, emodule.ModuleName, sdk.Coins{msg.Deposit.Amount}). + Return(nil).Once() + }) + res, err := suite.dhandler(suite.ctx, msg) require.NoError(t, err) require.NotNil(t, res) @@ -203,6 +206,22 @@ func TestCreateDeployment(t *testing.T) { res, err = suite.dhandler(suite.ctx, msg) require.EqualError(t, err, v1.ErrDeploymentExists.Error()) require.Nil(t, res) + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, emodule.ModuleName, owner, sdk.Coins{msg.Deposit.Amount}). + Return(nil).Once() + }) + + cmsg := &v1beta4.MsgCloseDeployment{ + ID: deployment.ID, + } + + res, err = suite.dhandler(suite.ctx, cmsg) + require.NoError(t, err) + require.NotNil(t, res) } func TestCreateDeploymentEmptyGroups(t *testing.T) { @@ -259,6 +278,16 @@ func TestUpdateDeploymentExisting(t *testing.T) { }, } + owner := sdk.MustAccAddressFromBech32(deployment.ID.Owner) + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, owner, emodule.ModuleName, sdk.Coins{msg.Deposit.Amount}). + Return(nil).Once() + }) + res, err := suite.dhandler(suite.ctx, msg) require.NoError(t, err) require.NotNil(t, res) @@ -335,6 +364,15 @@ func TestCloseDeploymentExisting(t *testing.T) { msg.Groups = append(msg.Groups, group.GroupSpec) } + owner := sdk.MustAccAddressFromBech32(deployment.ID.Owner) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, owner, emodule.ModuleName, sdk.Coins{msg.Deposit.Amount}). + Return(nil).Once() + }) + res, err := suite.dhandler(suite.ctx, msg) require.NoError(t, err) require.NotNil(t, res) @@ -354,6 +392,14 @@ func TestCloseDeploymentExisting(t *testing.T) { ID: deployment.ID, } + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, emodule.ModuleName, owner, mock.Anything). + Return(nil).Once() + }) + res, err = suite.dhandler(suite.ctx, msgClose) require.NotNil(t, res) require.NoError(t, err) @@ -394,6 +440,13 @@ func TestFundedDeployment(t *testing.T) { msg.Groups = append(msg.Groups, group.GroupSpec) } + //owner := sdk.MustAccAddressFromBech32(deployment.ID.Owner) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, emodule.ModuleName, sdk.Coins{msg.Deposit.Amount}). + Return(nil).Once() + }) res, err := suite.dhandler(suite.ctx, msg) require.NoError(t, err) require.NotNil(t, res) @@ -426,6 +479,14 @@ func TestFundedDeployment(t *testing.T) { Sources: deposit.Sources{deposit.SourceBalance}, }, } + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, emodule.ModuleName, sdk.Coins{depositMsg.Deposit.Amount}). + Return(nil).Once() + }) + res, err = suite.ehandler(suite.ctx, depositMsg) require.NoError(t, err) require.NotNil(t, res) @@ -451,6 +512,13 @@ func TestFundedDeployment(t *testing.T) { Sources: deposit.Sources{deposit.SourceGrant}, }, } + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, emodule.ModuleName, sdk.Coins{depositMsg1.Deposit.Amount}). + Return(nil).Once() + }) res, err = suite.ehandler(suite.ctx, depositMsg1) require.NoError(t, err) require.NotNil(t, res) @@ -468,14 +536,23 @@ func TestFundedDeployment(t *testing.T) { require.Equal(t, fundsAmount, acc.State.Funds[0].Amount) // depositing additional amount from a random depositor should pass + rndDepositor := testutil.AccAddress(t) + depositMsg2 := &ev1.MsgAccountDeposit{ - Signer: testutil.AccAddress(t).String(), + Signer: rndDepositor.String(), ID: deployment.ID.ToEscrowAccountID(), Deposit: deposit.Deposit{ Amount: suite.defaultDeposit, Sources: deposit.Sources{deposit.SourceBalance}, }, } + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, emodule.ModuleName, sdk.Coins{depositMsg2.Deposit.Amount}). + Return(nil).Once() + }) res, err = suite.ehandler(suite.ctx, depositMsg2) require.NoError(t, err) require.NotNil(t, res) @@ -511,6 +588,15 @@ func TestFundedDeployment(t *testing.T) { ctx := suite.ctx.WithBlockHeight(acc.State.SettledAt + 1) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, emodule.ModuleName, distrtypes.ModuleName, sdk.Coins{sdk.NewInt64Coin(depositMsg.Deposit.Amount.Denom, 10_000)}). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, emodule.ModuleName, mock.Anything, sdk.NewCoins(testutil.AkashCoin(t, 490_000))). + Return(nil).Once() + }) + err = suite.EscrowKeeper().PaymentWithdraw(ctx, pid) require.NoError(t, err) @@ -527,6 +613,21 @@ func TestFundedDeployment(t *testing.T) { // close the deployment closeMsg := &v1beta4.MsgCloseDeployment{ID: deployment.ID} + + owner := sdk.MustAccAddressFromBech32(deployment.ID.Owner) + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, emodule.ModuleName, owner, sdk.NewCoins(testutil.AkashCoin(t, 500_000))). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, emodule.ModuleName, suite.granter, sdk.NewCoins(testutil.AkashCoin(t, 500_000))). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, emodule.ModuleName, rndDepositor, sdk.NewCoins(testutil.AkashCoin(t, 500_000))). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, emodule.ModuleName, providerAddr, sdk.NewCoins(testutil.AkashCoin(t, 500_000))). + Return(nil).Once() + }) res, err = suite.dhandler(ctx, closeMsg) require.NoError(t, err) require.NotNil(t, res) diff --git a/x/deployment/keeper/grpc_query_test.go b/x/deployment/keeper/grpc_query_test.go index f48a4baf7d..cdcb093dc7 100644 --- a/x/deployment/keeper/grpc_query_test.go +++ b/x/deployment/keeper/grpc_query_test.go @@ -10,6 +10,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkquery "github.com/cosmos/cosmos-sdk/types/query" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" deposit "pkg.akt.dev/go/node/types/deposit/v1" @@ -25,6 +26,7 @@ import ( ) type grpcTestSuite struct { + *state.TestSuite t *testing.T app *app.AkashApp ctx sdk.Context @@ -39,6 +41,7 @@ type grpcTestSuite struct { func setupTest(t *testing.T) *grpcTestSuite { ssuite := state.SetupTestSuite(t) suite := &grpcTestSuite{ + TestSuite: ssuite, t: t, app: ssuite.App(), ctx: ssuite.Context(), @@ -60,6 +63,20 @@ func setupTest(t *testing.T) *grpcTestSuite { func TestGRPCQueryDeployment(t *testing.T) { suite := setupTest(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) + // creating deployment deployment, groups := suite.createDeployment() err := suite.keeper.Create(suite.ctx, deployment, groups) @@ -67,10 +84,8 @@ func TestGRPCQueryDeployment(t *testing.T) { eid := suite.createEscrowAccount(deployment.ID) - var ( - req *v1beta4.QueryDeploymentRequest - expDeployment v1beta4.QueryDeploymentResponse - ) + var req *v1beta4.QueryDeploymentRequest + var expDeployment v1beta4.QueryDeploymentResponse testCases := []struct { msg string @@ -138,6 +153,19 @@ func TestGRPCQueryDeployment(t *testing.T) { func TestGRPCQueryDeployments(t *testing.T) { suite := setupTest(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) // creating deployments with different states deployment, groups := suite.createDeployment() @@ -258,6 +286,19 @@ type deploymentFilterModifier struct { func TestGRPCQueryDeploymentsWithFilter(t *testing.T) { suite := setupTest(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) // creating orders with different states depA, _ := createActiveDeployment(t, suite.ctx, suite.keeper) @@ -452,6 +493,20 @@ func TestGRPCQueryDeploymentsWithFilter(t *testing.T) { func TestGRPCQueryGroup(t *testing.T) { suite := setupTest(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) + // creating group deployment, groups := suite.createDeployment() err := suite.keeper.Create(suite.ctx, deployment, groups) @@ -525,6 +580,20 @@ func TestGRPCQueryGroup(t *testing.T) { func (suite *grpcTestSuite) createDeployment() (v1.Deployment, v1beta4.Groups) { suite.t.Helper() + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) + deployment := testutil.Deployment(suite.t) group := testutil.DeploymentGroup(suite.t, deployment.ID, 0) group.GroupSpec.Resources = v1beta4.ResourceUnits{ diff --git a/x/escrow/genesis.go b/x/escrow/genesis.go index 4e276d6c4e..07eb8152b5 100644 --- a/x/escrow/genesis.go +++ b/x/escrow/genesis.go @@ -73,7 +73,10 @@ func InitGenesis(ctx sdk.Context, keeper keeper.Keeper, data *types.GenesisState } } for idx := range data.Payments { - keeper.SavePayment(ctx, data.Payments[idx]) + err := keeper.SavePayment(ctx, data.Payments[idx]) + if err != nil { + panic(fmt.Sprintf("error saving payment: %s", err.Error())) + } } } diff --git a/x/escrow/keeper/grpc_query.go b/x/escrow/keeper/grpc_query.go index 92233f491f..256bd3ba3d 100644 --- a/x/escrow/keeper/grpc_query.go +++ b/x/escrow/keeper/grpc_query.go @@ -81,7 +81,7 @@ func (k Querier) Accounts(c context.Context, req *v1.QueryAccountsRequest) (*v1. if len(req.Pagination.Key) == 0 { req.State = state.String() - searchPrefix = buildSearchPrefix(AccountPrefix, req.State, req.XID) + searchPrefix = BuildSearchPrefix(AccountPrefix, req.State, req.XID) } searchStore := prefix.NewStore(ctx.KVStore(k.skey), searchPrefix) @@ -193,7 +193,7 @@ func (k Querier) Payments(c context.Context, req *v1.QueryPaymentsRequest) (*v1. if len(req.Pagination.Key) == 0 { req.State = state.String() - searchPrefix = buildSearchPrefix(PaymentPrefix, req.State, req.XID) + searchPrefix = BuildSearchPrefix(PaymentPrefix, req.State, req.XID) } searchStore := prefix.NewStore(ctx.KVStore(k.skey), searchPrefix) @@ -247,7 +247,7 @@ func (k Querier) Payments(c context.Context, req *v1.QueryPaymentsRequest) (*v1. }, nil } -func buildSearchPrefix(prefix []byte, state string, xid string) []byte { +func BuildSearchPrefix(prefix []byte, state string, xid string) []byte { buf := &bytes.Buffer{} buf.Write(prefix) diff --git a/x/escrow/keeper/grpc_query_test.go b/x/escrow/keeper/grpc_query_test.go index b2b2e8ed4f..56fa1dde4e 100644 --- a/x/escrow/keeper/grpc_query_test.go +++ b/x/escrow/keeper/grpc_query_test.go @@ -5,6 +5,7 @@ import ( "testing" sdkmath "cosmossdk.io/math" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/cosmos/cosmos-sdk/baseapp" @@ -25,6 +26,7 @@ import ( ) type grpcTestSuite struct { + *state.TestSuite t *testing.T app *app.AkashApp ctx sdk.Context @@ -38,6 +40,8 @@ type grpcTestSuite struct { func setupTest(t *testing.T) *grpcTestSuite { ssuite := state.SetupTestSuite(t) suite := &grpcTestSuite{ + TestSuite: ssuite, + t: t, app: ssuite.App(), ctx: ssuite.Context(), @@ -263,6 +267,20 @@ func TestGRPCQueryPayments(t *testing.T) { } func (suite *grpcTestSuite) createEscrowAccount(id dv1.DeploymentID) eid.Account { + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) + owner, err := sdk.AccAddressFromBech32(id.Owner) require.NoError(suite.t, err) diff --git a/x/escrow/keeper/keeper.go b/x/escrow/keeper/keeper.go index 6321f7b692..3bafde23ef 100644 --- a/x/escrow/keeper/keeper.go +++ b/x/escrow/keeper/keeper.go @@ -24,8 +24,8 @@ import ( deposit "pkg.akt.dev/go/node/types/deposit/v1" ) -type AccountHook func(sdk.Context, etypes.Account) -type PaymentHook func(sdk.Context, etypes.Payment) +type AccountHook func(sdk.Context, etypes.Account) error +type PaymentHook func(sdk.Context, etypes.Payment) error type Keeper interface { Codec() codec.BinaryCodec @@ -45,7 +45,7 @@ type Keeper interface { WithAccounts(sdk.Context, func(etypes.Account) bool) WithPayments(sdk.Context, func(etypes.Payment) bool) SaveAccount(sdk.Context, etypes.Account) error - SavePayment(sdk.Context, etypes.Payment) + SavePayment(sdk.Context, etypes.Payment) error NewQuerier() Querier } @@ -243,7 +243,7 @@ func (k *keeper) AuthorizeDeposits(sctx sdk.Context, msg sdk.Msg) ([]etypes.Depo return false } - // Delete is ignored here as not all fund may be used during deployment lifetime. + // Delete is ignored here as not all funds may be used during deployment lifetime. // also, there can be another deployment using same authorization and may return funds before deposit is fully used err = k.authzKeeper.SaveGrant(ctx, owner, granter, resp.Updated, expiration) if err != nil { @@ -284,22 +284,43 @@ func (k *keeper) AuthorizeDeposits(sctx sdk.Context, msg sdk.Msg) ([]etypes.Depo } func (k *keeper) AccountClose(ctx sdk.Context, id escrowid.Account) error { - acc, payments, od, err := k.accountSettle(ctx, id) + acc, err := k.getAccount(ctx, id) if err != nil { return err } - if od { - return nil + switch acc.State.State { + case etypes.StateOpen: + case etypes.StateOverdrawn: + // if account is overdrawn try to settle it + // if settling fails it s still triggers deployment close + case etypes.StateClosed: + fallthrough + default: + return module.ErrAccountClosed + } + + // ignore overdraft return value + // all objects have correct values set + payments, _, err := k.accountSettle(ctx, acc) + if err != nil { + return err } - acc.State.State = etypes.StateClosed acc.dirty = true + // call to accountSettle above will set account and payments to overdrawn state + if acc.State.State == etypes.StateOpen { + acc.State.State = etypes.StateClosed + } + for idx := range payments { - payments[idx].State.State = etypes.StateClosed payments[idx].dirty = true + if payments[idx].State.State == etypes.StateOpen { + payments[idx].State.State = etypes.StateClosed + } + if err := k.paymentWithdraw(ctx, &payments[idx]); err != nil { return err } @@ -314,21 +335,47 @@ func (k *keeper) AccountClose(ctx sdk.Context, id escrowid.Account) error { } func (k *keeper) AccountDeposit(ctx sdk.Context, id escrowid.Account, deposits []etypes.Depositor) error { - obj, err := k.getAccount(ctx, id) + acc, err := k.getAccount(ctx, id) if err != nil { return err } - if obj.State.State != etypes.StateOpen { + if acc.State.State == etypes.StateClosed { return module.ErrAccountClosed } - if err := k.fetchDepositsToAccount(ctx, &obj, deposits); err != nil { + if err = k.fetchDepositsToAccount(ctx, acc, deposits); err != nil { return err } - if obj.dirty { - err = k.saveAccount(ctx, obj) + if acc.State.State == etypes.StateOverdrawn { + payments, od, err := k.accountSettle(ctx, acc) + if err != nil { + return err + } + + for idx := range payments { + payments[idx].dirty = true + + if payments[idx].State.State == etypes.StateOpen { + payments[idx].State.State = etypes.StateClosed + } + + if err := k.paymentWithdraw(ctx, &payments[idx]); err != nil { + return err + } + } + + if !od { + acc.State.State = etypes.StateClosed + } + + err = k.save(ctx, acc, payments) + if err != nil { + return err + } + } else if acc.dirty { + err = k.saveAccount(ctx, acc) if err != nil { return err } @@ -338,7 +385,12 @@ func (k *keeper) AccountDeposit(ctx sdk.Context, id escrowid.Account, deposits [ } func (k *keeper) AccountSettle(ctx sdk.Context, id escrowid.Account) (bool, error) { - acc, payments, od, err := k.accountSettle(ctx, id) + acc, err := k.getAccount(ctx, id) + if err != nil { + return false, err + } + + payments, od, err := k.accountSettle(ctx, acc) if err != nil { return false, err } @@ -376,7 +428,14 @@ func (k *keeper) fetchDepositsToAccount(ctx sdk.Context, acc *account, deposits return module.ErrInvalidDenomination } - if err := k.bkeeper.SendCoinsFromAccountToModule(ctx, depositor, module.ModuleName, sdk.NewCoins(sdk.NewCoin(d.Balance.Denom, d.Balance.Amount.TruncateInt()))); err != nil { + if funds.Amount.IsNegative() { + funds.Amount = sdkmath.LegacyZeroDec() + } + + // if balance is negative then reset it to zero and start accumulating fund. + // later down in this function it will trigger account settlement and recalculate + // the owed balance + if err = k.bkeeper.SendCoinsFromAccountToModule(ctx, depositor, module.ModuleName, sdk.NewCoins(sdk.NewCoin(d.Balance.Denom, d.Balance.Amount.TruncateInt()))); err != nil { return err } @@ -388,36 +447,44 @@ func (k *keeper) fetchDepositsToAccount(ctx sdk.Context, acc *account, deposits return nil } -func (k *keeper) accountSettle(ctx sdk.Context, id escrowid.Account) (account, []payment, bool, error) { - acc, err := k.getAccount(ctx, id) - if err != nil { - return account{}, nil, false, err +func (k *keeper) accountSettle(ctx sdk.Context, acc *account) ([]payment, bool, error) { + if acc.State.State == etypes.StateClosed { + return nil, false, module.ErrAccountClosed } - if acc.State.State != etypes.StateOpen { - return account{}, nil, false, module.ErrAccountClosed + if acc.State.Funds[0].Amount.IsNegative() { + return nil, true, nil } - payments := k.accountOpenPayments(ctx, id) + // overdrawn account does not update settledAt, as associated objects like deployment + // are closed + heightDelta := sdkmath.NewInt(0) + if acc.State.State != etypes.StateOverdrawn { + heightDelta = heightDelta.AddRaw(ctx.BlockHeight() - acc.State.SettledAt) + acc.State.SettledAt = ctx.BlockHeight() + } + + pStates := []etypes.State{ + etypes.StateOverdrawn, + } - heightDelta := sdkmath.NewInt(ctx.BlockHeight() - acc.State.SettledAt) - if heightDelta.IsZero() { - return acc, nil, false, nil + if !heightDelta.IsZero() { + pStates = append(pStates, etypes.StateOpen) } - acc.State.SettledAt = ctx.BlockHeight() acc.dirty = true + payments := k.accountPayments(ctx, acc.ID, pStates) if len(payments) == 0 { - return acc, nil, false, nil + return nil, false, nil } - overdrawn := accountSettleFullBlocks(&acc, payments, heightDelta) + overdrawn := accountSettleFullBlocks(acc, payments, heightDelta) // all payments made in full if !overdrawn { // return early - return acc, payments, false, nil + return payments, false, nil } // @@ -429,15 +496,20 @@ func (k *keeper) accountSettle(ctx sdk.Context, id escrowid.Account) (account, [ payments[idx].State.State = etypes.StateOverdrawn payments[idx].dirty = true if err := k.paymentWithdraw(ctx, &payments[idx]); err != nil { - return acc, payments, overdrawn, err + return payments, overdrawn, err } } - return acc, payments, overdrawn, nil + return payments, overdrawn, nil } func (k *keeper) PaymentCreate(ctx sdk.Context, id escrowid.Payment, owner sdk.AccAddress, rate sdk.DecCoin) error { - acc, _, od, err := k.accountSettle(ctx, id.Account()) + acc, err := k.getAccount(ctx, id.Account()) + if err != nil { + return err + } + + _, od, err := k.accountSettle(ctx, acc) if err != nil { return err } @@ -474,7 +546,7 @@ func (k *keeper) PaymentCreate(ctx sdk.Context, id escrowid.Payment, owner sdk.A } } - k.savePayment(ctx, payment{ + err = k.savePayment(ctx, payment{ Payment: etypes.Payment{ ID: id, State: etypes.PaymentState{ @@ -489,10 +561,19 @@ func (k *keeper) PaymentCreate(ctx sdk.Context, id escrowid.Payment, owner sdk.A prevState: etypes.StateOpen, }) + if err != nil { + return err + } + return nil } func (k *keeper) PaymentWithdraw(ctx sdk.Context, id escrowid.Payment) error { + acc, err := k.getAccount(ctx, id.Account()) + if err != nil { + return err + } + pmnt, err := k.getPayment(ctx, id) if err != nil { return err @@ -502,7 +583,7 @@ func (k *keeper) PaymentWithdraw(ctx sdk.Context, id escrowid.Payment) error { return module.ErrPaymentClosed } - acc, payments, od, err := k.accountSettle(ctx, id.Account()) + payments, od, err := k.accountSettle(ctx, acc) if err != nil { return err } @@ -535,42 +616,48 @@ func (k *keeper) PaymentWithdraw(ctx sdk.Context, id escrowid.Payment) error { } func (k *keeper) PaymentClose(ctx sdk.Context, id escrowid.Payment) error { + acc, err := k.getAccount(ctx, id.Account()) + if err != nil { + return err + } + pmnt, err := k.getPayment(ctx, id) if err != nil { return err } - if pmnt.State.State != etypes.StateOpen { + switch pmnt.State.State { + case etypes.StateOpen: + case etypes.StateOverdrawn: + // if payment is overdrawn try to settle it + // if settling fails it s still triggers deployment close + case etypes.StateClosed: + fallthrough + default: return module.ErrPaymentClosed } - acc, payments, od, err := k.accountSettle(ctx, id.Account()) + payments, _, err := k.accountSettle(ctx, acc) if err != nil { return err } - if od { - return nil - } - pmnt = nil + acc.dirty = true - for i, p := range payments { - if p.ID.Key() == id.Key() { - pmnt = &payments[i] - } - } + for idx := range payments { + payments[idx].dirty = true - if pmnt == nil { - panic(fmt.Sprintf("couldn't find payment %s", id.String())) - } + if payments[idx].ID.String() == pmnt.ID.String() { + if payments[idx].State.State == etypes.StateOpen { + payments[idx].State.State = etypes.StateClosed + } - if err := k.paymentWithdraw(ctx, pmnt); err != nil { - return err + if err := k.paymentWithdraw(ctx, &payments[idx]); err != nil { + return err + } + } } - pmnt.State.State = etypes.StateClosed - pmnt.dirty = true - err = k.save(ctx, acc, payments) if err != nil { return err @@ -598,17 +685,17 @@ func (k *keeper) GetAccount(ctx sdk.Context, id escrowid.Account) (etypes.Accoun return obj.Account, nil } -func (k *keeper) getAccount(ctx sdk.Context, id escrowid.Account) (account, error) { +func (k *keeper) getAccount(ctx sdk.Context, id escrowid.Account) (*account, error) { store := ctx.KVStore(k.skey) key := k.findAccount(ctx, &id) if len(key) == 0 { - return account{}, module.ErrAccountNotFound + return &account{}, module.ErrAccountNotFound } buf := store.Get(key) - obj := account{ + obj := &account{ Account: etypes.Account{ ID: id, }, @@ -652,7 +739,7 @@ func (k *keeper) getPayment(ctx sdk.Context, id escrowid.Payment) (*payment, err } func (k *keeper) SaveAccount(ctx sdk.Context, obj etypes.Account) error { - err := k.saveAccount(ctx, account{ + err := k.saveAccount(ctx, &account{ Account: obj, prevState: obj.State.State, }) @@ -664,8 +751,8 @@ func (k *keeper) SaveAccount(ctx sdk.Context, obj etypes.Account) error { return nil } -func (k *keeper) SavePayment(ctx sdk.Context, obj etypes.Payment) { - k.savePayment(ctx, payment{ +func (k *keeper) SavePayment(ctx sdk.Context, obj etypes.Payment) error { + return k.savePayment(ctx, payment{ Payment: obj, prevState: obj.State.State, }) @@ -712,7 +799,7 @@ func (k *keeper) WithPayments(ctx sdk.Context, fn func(etypes.Payment) bool) { } } -func (k *keeper) saveAccount(ctx sdk.Context, obj account) error { +func (k *keeper) saveAccount(ctx sdk.Context, obj *account) error { store := ctx.KVStore(k.skey) var key []byte @@ -772,14 +859,17 @@ func (k *keeper) saveAccount(ctx sdk.Context, obj account) error { if obj.State.State == etypes.StateClosed || obj.State.State == etypes.StateOverdrawn { // call hooks for _, hook := range k.hooks.onAccountClosed { - hook(ctx, obj.Account) + err := hook(ctx, obj.Account) + if err != nil { + return err + } } } return nil } -func (k *keeper) savePayment(ctx sdk.Context, obj payment) { +func (k *keeper) savePayment(ctx sdk.Context, obj payment) error { store := ctx.KVStore(k.skey) var key []byte @@ -794,12 +884,17 @@ func (k *keeper) savePayment(ctx sdk.Context, obj payment) { if obj.State.State == etypes.StateClosed || obj.State.State == etypes.StateOverdrawn { // call hooks for _, hook := range k.hooks.onPaymentClosed { - hook(ctx, obj.Payment) + err := hook(ctx, obj.Payment) + if err != nil { + return err + } } } + + return nil } -func (k *keeper) save(ctx sdk.Context, acc account, payments []payment) error { +func (k *keeper) save(ctx sdk.Context, acc *account, payments []payment) error { if acc.dirty { err := k.saveAccount(ctx, acc) if err != nil { @@ -809,34 +904,44 @@ func (k *keeper) save(ctx sdk.Context, acc account, payments []payment) error { for _, pmnt := range payments { if pmnt.dirty { - k.savePayment(ctx, pmnt) + err := k.savePayment(ctx, pmnt) + if err != nil { + return err + } } } return nil } -func (k *keeper) accountOpenPayments(ctx sdk.Context, id escrowid.Account) []payment { +func (k *keeper) accountPayments(ctx sdk.Context, id escrowid.Account, states []etypes.State) []payment { store := ctx.KVStore(k.skey) - prefix := BuildPaymentsKey(etypes.StateOpen, &id) - iter := storetypes.KVStorePrefixIterator(store, prefix) - - var payments []payment + iters := make([]storetypes.Iterator, 0, len(states)) defer func() { - _ = iter.Close() + for _, iter := range iters { + _ = iter.Close() + } }() - for ; iter.Valid(); iter.Next() { - id, _ := ParsePaymentKey(iter.Key()) - val := etypes.Payment{ - ID: id, + var payments []payment + + for _, state := range states { + prefix := BuildPaymentsKey(state, &id) + iter := storetypes.KVStorePrefixIterator(store, prefix) + iters = append(iters, iter) + + for ; iter.Valid(); iter.Next() { + id, _ := ParsePaymentKey(iter.Key()) + val := etypes.Payment{ + ID: id, + } + k.cdc.MustUnmarshal(iter.Value(), &val.State) + payments = append(payments, payment{ + Payment: val, + prevState: val.State.State, + }) } - k.cdc.MustUnmarshal(iter.Value(), &val.State) - payments = append(payments, payment{ - Payment: val, - prevState: val.State.State, - }) } return payments @@ -859,13 +964,13 @@ func (k *keeper) paymentWithdraw(ctx sdk.Context, obj *payment) error { return err } - if err := k.sendFeeToCommunityPool(ctx, fee); err != nil { + if err = k.sendFeeToCommunityPool(ctx, fee); err != nil { ctx.Logger().Error("payment withdraw - fees", "err", err, "id", obj.ID.Key()) return err } if !earnings.IsZero() { - if err := k.bkeeper.SendCoinsFromModuleToAccount(ctx, module.ModuleName, owner, sdk.NewCoins(earnings)); err != nil { + if err = k.bkeeper.SendCoinsFromModuleToAccount(ctx, module.ModuleName, owner, sdk.NewCoins(earnings)); err != nil { ctx.Logger().Error("payment withdraw - earnings", "err", err, "is", obj.ID.Key()) return err } @@ -941,8 +1046,6 @@ func (acc *account) deductFromBalance(amount sdk.DecCoin) (sdk.DecCoin, bool) { toWithdraw.AddMut(remaining) } - funds.Amount.SubMut(toWithdraw) - acc.State.Deposits[i].Balance.Amount.SubMut(toWithdraw) if acc.State.Deposits[i].Balance.IsZero() { idx++ @@ -956,16 +1059,19 @@ func (acc *account) deductFromBalance(amount sdk.DecCoin) (sdk.DecCoin, bool) { } } + // clean empty deposits if idx > 0 { acc.State.Deposits = acc.State.Deposits[idx:] } + funds.Amount.SubMut(withdrew) res := sdk.NewDecCoinFromDec(amount.Denom, withdrew) if remaining.IsZero() { return res, false } + // at this point the account is overdrawn funds.Amount.SubMut(remaining) return res, true @@ -982,27 +1088,40 @@ func accountSettleFullBlocks(acc *account, payments []payment, heightDelta sdkma } for idx := range payments { - p := payments[idx] - paymentTransfer := sdk.NewDecCoinFromDec(p.State.Rate.Denom, p.State.Rate.Amount.Mul(sdkmath.LegacyNewDecFromInt(heightDelta))) + p := &payments[idx] + + var transfer sdk.DecCoin + + if p.prevState == etypes.StateOverdrawn { + transfer = sdk.NewDecCoinFromDec(p.State.Unsettled.Denom, sdkmath.LegacyZeroDec()) + transfer.Amount.AddMut(p.State.Unsettled.Amount) - paymentsTransfers = append(paymentsTransfers, paymentTransfer) + p.State.Unsettled.Amount = sdkmath.LegacyZeroDec() + } else { + transfer = sdk.NewDecCoinFromDec(p.State.Rate.Denom, p.State.Rate.Amount.Mul(sdkmath.LegacyNewDecFromInt(heightDelta)).TruncateDec()) + } + paymentsTransfers = append(paymentsTransfers, transfer) } overdrawn := false for idx := range payments { unsettledAmount := paymentsTransfers[idx] - settledAmount, od := acc.deductFromBalance(unsettledAmount) - unsettledAmount.Amount.SubMut(unsettledAmount.Amount) + unsettledAmount.Amount.SubMut(settledAmount.Amount) + + payments[idx].dirty = true if settledAmount.IsPositive() { payments[idx].State.Balance.Amount.AddMut(settledAmount.Amount) } if od { overdrawn = true + payments[idx].State.State = etypes.StateOverdrawn payments[idx].State.Unsettled.Amount.AddMut(unsettledAmount.Amount) + } else { + payments[idx].State.State = etypes.StateOpen } } diff --git a/x/escrow/keeper/keeper_settle_test.go b/x/escrow/keeper/keeper_settle_test.go index 960fa781b8..8398a01922 100644 --- a/x/escrow/keeper/keeper_settle_test.go +++ b/x/escrow/keeper/keeper_settle_test.go @@ -1,6 +1,7 @@ package keeper import ( + "fmt" "testing" sdkmath "cosmossdk.io/math" @@ -106,8 +107,35 @@ func TestSettleFullBlocks(t *testing.T) { assertAmountEqual(t, sdkmath.LegacyNewDec(tt.cfg.balanceEnd), account.State.Funds[0].Amount, tt.name) + totalTransferred := sdkmath.LegacyZeroDec() + for _, transfer := range tt.cfg.transferred { + totalTransferred.AddMut(transfer) + } + + assert.Equal(t, totalTransferred, account.State.Transferred[0].Amount, fmt.Sprintf("%s: deposit balance should be decremented by total transferred", tt.name)) + + totalPayments := sdkmath.LegacyZeroDec() + totalUnsettled := sdkmath.LegacyZeroDec() + for idx := range payments { assert.Equal(t, sdk.NewDecCoinFromDec(denom, tt.cfg.transferred[idx]), payments[idx].State.Balance, tt.name) + totalPayments.AddMut(payments[idx].State.Balance.Amount) + totalPayments.AddMut(payments[idx].State.Unsettled.Amount) + totalUnsettled.AddMut(payments[idx].State.Unsettled.Amount) + } + + // Check that funds were decremented by the total payments amount + expectedRemainingBalance := sdkmath.LegacyNewDec(tt.cfg.balanceStart).Sub(totalPayments) + + assert.Equal(t, expectedRemainingBalance, account.State.Funds[0].Amount, fmt.Sprintf("%s: deposit balance should be decremented by total payments", tt.name)) + + // Check unsettled amounts are tracked when overdrawn + if overdrawn { + // balance expected to be negative + assert.True(t, account.State.Funds[0].Amount.IsNegative()) + + unsettledDiff := account.State.Funds[0].Amount.Add(totalUnsettled) + assert.Equal(t, sdkmath.LegacyZeroDec().String(), unsettledDiff.String()) } } } diff --git a/x/escrow/keeper/keeper_test.go b/x/escrow/keeper/keeper_test.go index 427dfef6db..b7c14d1d30 100644 --- a/x/escrow/keeper/keeper_test.go +++ b/x/escrow/keeper/keeper_test.go @@ -3,20 +3,94 @@ package keeper_test import ( "testing" + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "pkg.akt.dev/go/node/escrow/module" etypes "pkg.akt.dev/go/node/escrow/types/v1" "pkg.akt.dev/go/testutil" - cmocks "pkg.akt.dev/node/testutil/cosmos/mocks" "pkg.akt.dev/node/testutil/state" - "pkg.akt.dev/node/x/escrow/keeper" ) +type kTestSuite struct { + *state.TestSuite +} + +func Test_AccountSettlement(t *testing.T) { + ssuite := state.SetupTestSuite(t) + ctx := ssuite.Context() + + bkeeper := ssuite.BankKeeper() + ekeeper := ssuite.EscrowKeeper() + + lid := testutil.LeaseID(t) + did := lid.DeploymentID() + + aid := did.ToEscrowAccountID() + pid := lid.ToEscrowPaymentID() + + aowner := testutil.AccAddress(t) + + amt := testutil.AkashCoin(t, 1000) + powner := testutil.AccAddress(t) + rate := testutil.AkashCoin(t, 10) + + // create an account + bkeeper. + On("SendCoinsFromAccountToModule", ctx, aowner, module.ModuleName, sdk.NewCoins(amt)). + Return(nil).Once() + assert.NoError(t, ekeeper.AccountCreate(ctx, aid, aowner, []etypes.Depositor{{ + Owner: aowner.String(), + Height: ctx.BlockHeight(), + Balance: sdk.NewDecCoinFromCoin(amt), + }})) + + { + acct, err := ekeeper.GetAccount(ctx, aid) + require.NoError(t, err) + require.Equal(t, ctx.BlockHeight(), acct.State.SettledAt) + } + + // create payment + err := ekeeper.PaymentCreate(ctx, pid, powner, sdk.NewDecCoinFromCoin(rate)) + assert.NoError(t, err) + + blkdelta := int64(10) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + blkdelta) + // trigger settlement by closing the account, + // 2% is take rate, which in this test equals 2 + // 98 uakt is payment amount + // 900 uakt must be returned to the aowner + + bkeeper. + On("SendCoinsFromModuleToModule", ctx, module.ModuleName, distrtypes.ModuleName, sdk.NewCoins(testutil.AkashCoin(t, 2))). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, module.ModuleName, powner, sdk.NewCoins(testutil.AkashCoin(t, (rate.Amount.Int64()*10)-2))). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, module.ModuleName, aowner, sdk.NewCoins(testutil.AkashCoin(t, amt.Amount.Int64()-(rate.Amount.Int64()*10)))). + Return(nil).Once() + err = ekeeper.AccountClose(ctx, aid) + assert.NoError(t, err) + + acct, err := ekeeper.GetAccount(ctx, aid) + require.NoError(t, err) + require.Equal(t, ctx.BlockHeight(), acct.State.SettledAt) + require.Equal(t, etypes.StateClosed, acct.State.State) + require.Equal(t, testutil.AkashDecCoin(t, rate.Amount.Int64()*ctx.BlockHeight()), acct.State.Transferred[0]) +} + func Test_AccountCreate(t *testing.T) { - ctx, keeper, bkeeper := setupKeeper(t) + ssuite := state.SetupTestSuite(t) + ctx := ssuite.Context() + + bkeeper := ssuite.BankKeeper() + ekeeper := ssuite.EscrowKeeper() + id := testutil.DeploymentID(t).ToEscrowAccountID() owner := testutil.AccAddress(t) @@ -25,9 +99,9 @@ func Test_AccountCreate(t *testing.T) { // create account bkeeper. - On("SendCoinsFromAccountToModule", ctx, owner, module.ModuleName, sdk.NewCoins(amt)). - Return(nil) - assert.NoError(t, keeper.AccountCreate(ctx, id, owner, []etypes.Depositor{{ + On("SendCoinsFromAccountToModule", mock.Anything, owner, module.ModuleName, sdk.NewCoins(amt)). + Return(nil).Once() + assert.NoError(t, ekeeper.AccountCreate(ctx, id, owner, []etypes.Depositor{{ Owner: owner.String(), Height: ctx.BlockHeight(), Balance: sdk.NewDecCoinFromCoin(amt), @@ -36,30 +110,35 @@ func Test_AccountCreate(t *testing.T) { // deposit more tokens ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 10) bkeeper. - On("SendCoinsFromAccountToModule", ctx, owner, module.ModuleName, sdk.NewCoins(amt2)). - Return(nil) - assert.NoError(t, keeper.AccountDeposit(ctx, id, []etypes.Depositor{{ + On("SendCoinsFromAccountToModule", mock.Anything, owner, module.ModuleName, sdk.NewCoins(amt2)). + Return(nil).Once() + + assert.NoError(t, ekeeper.AccountDeposit(ctx, id, []etypes.Depositor{{ Owner: owner.String(), Height: ctx.BlockHeight(), Balance: sdk.NewDecCoinFromCoin(amt2), }})) // close account + // each deposit is it's own send ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 10) bkeeper. - On("SendCoinsFromModuleToAccount", ctx, module.ModuleName, owner, sdk.NewCoins(amt.Add(amt2))). - Return(nil) - assert.NoError(t, keeper.AccountClose(ctx, id)) + On("SendCoinsFromModuleToAccount", mock.Anything, module.ModuleName, owner, sdk.NewCoins(amt)). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, module.ModuleName, owner, sdk.NewCoins(amt2)). + Return(nil).Once() + + assert.NoError(t, ekeeper.AccountClose(ctx, id)) // no deposits after closed - assert.Error(t, keeper.AccountDeposit(ctx, id, []etypes.Depositor{{ + assert.Error(t, ekeeper.AccountDeposit(ctx, id, []etypes.Depositor{{ Owner: owner.String(), Height: ctx.BlockHeight(), Balance: sdk.NewDecCoinFromCoin(amt), }})) // no re-creating account - assert.Error(t, keeper.AccountCreate(ctx, id, owner, []etypes.Depositor{{ + assert.Error(t, ekeeper.AccountCreate(ctx, id, owner, []etypes.Depositor{{ Owner: owner.String(), Height: ctx.BlockHeight(), Balance: sdk.NewDecCoinFromCoin(amt), @@ -67,7 +146,12 @@ func Test_AccountCreate(t *testing.T) { } func Test_PaymentCreate(t *testing.T) { - ctx, keeper, bkeeper := setupKeeper(t) + ssuite := state.SetupTestSuite(t) + ctx := ssuite.Context() + + bkeeper := ssuite.BankKeeper() + ekeeper := ssuite.EscrowKeeper() + lid := testutil.LeaseID(t) did := lid.DeploymentID() @@ -83,34 +167,36 @@ func Test_PaymentCreate(t *testing.T) { // create account bkeeper. On("SendCoinsFromAccountToModule", ctx, aowner, module.ModuleName, sdk.NewCoins(amt)). - Return(nil) - assert.NoError(t, keeper.AccountCreate(ctx, aid, aowner, []etypes.Depositor{{ + Return(nil).Once() + assert.NoError(t, ekeeper.AccountCreate(ctx, aid, aowner, []etypes.Depositor{{ Owner: aowner.String(), Height: ctx.BlockHeight(), Balance: sdk.NewDecCoinFromCoin(amt), }})) { - acct, err := keeper.GetAccount(ctx, aid) + acct, err := ekeeper.GetAccount(ctx, aid) require.NoError(t, err) require.Equal(t, ctx.BlockHeight(), acct.State.SettledAt) } // create payment - err := keeper.PaymentCreate(ctx, pid, powner, sdk.NewDecCoinFromCoin(rate)) + err := ekeeper.PaymentCreate(ctx, pid, powner, sdk.NewDecCoinFromCoin(rate)) assert.NoError(t, err) // withdraw some funds blkdelta := int64(10) ctx = ctx.WithBlockHeight(ctx.BlockHeight() + blkdelta) bkeeper. - On("SendCoinsFromModuleToAccount", ctx, module.ModuleName, powner, sdk.NewCoins(testutil.AkashCoin(t, rate.Amount.Int64()*blkdelta))). - Return(nil) - err = keeper.PaymentWithdraw(ctx, pid) + On("SendCoinsFromModuleToModule", mock.Anything, module.ModuleName, distrtypes.ModuleName, sdk.NewCoins(testutil.AkashCoin(t, 2))). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, module.ModuleName, powner, sdk.NewCoins(testutil.AkashCoin(t, (rate.Amount.Int64()*blkdelta)-2))). + Return(nil).Once() + err = ekeeper.PaymentWithdraw(ctx, pid) assert.NoError(t, err) { - acct, err := keeper.GetAccount(ctx, aid) + acct, err := ekeeper.GetAccount(ctx, aid) require.NoError(t, err) require.Equal(t, ctx.BlockHeight(), acct.State.SettledAt) @@ -118,7 +204,7 @@ func Test_PaymentCreate(t *testing.T) { require.Equal(t, testutil.AkashDecCoin(t, amt.Amount.Int64()-rate.Amount.Int64()*ctx.BlockHeight()), sdk.NewDecCoinFromDec(acct.State.Funds[0].Denom, acct.State.Funds[0].Amount)) require.Equal(t, testutil.AkashDecCoin(t, rate.Amount.Int64()*ctx.BlockHeight()), acct.State.Transferred[0]) - payment, err := keeper.GetPayment(ctx, pid) + payment, err := ekeeper.GetPayment(ctx, pid) require.NoError(t, err) require.Equal(t, etypes.StateOpen, payment.State.State) @@ -130,12 +216,14 @@ func Test_PaymentCreate(t *testing.T) { blkdelta = 20 ctx = ctx.WithBlockHeight(ctx.BlockHeight() + blkdelta) bkeeper. - On("SendCoinsFromModuleToAccount", ctx, module.ModuleName, powner, sdk.NewCoins(testutil.AkashCoin(t, rate.Amount.Int64()*blkdelta))). - Return(nil) - assert.NoError(t, keeper.PaymentClose(ctx, pid)) + On("SendCoinsFromModuleToModule", mock.Anything, module.ModuleName, distrtypes.ModuleName, sdk.NewCoins(testutil.AkashCoin(t, 4))). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", ctx, module.ModuleName, powner, sdk.NewCoins(testutil.AkashCoin(t, (rate.Amount.Int64()*blkdelta)-4))). + Return(nil).Once() + assert.NoError(t, ekeeper.PaymentClose(ctx, pid)) { - acct, err := keeper.GetAccount(ctx, aid) + acct, err := ekeeper.GetAccount(ctx, aid) require.NoError(t, err) require.Equal(t, ctx.BlockHeight(), acct.State.SettledAt) @@ -143,7 +231,7 @@ func Test_PaymentCreate(t *testing.T) { require.Equal(t, testutil.AkashDecCoin(t, amt.Amount.Int64()-rate.Amount.Int64()*ctx.BlockHeight()), sdk.NewDecCoinFromDec(acct.State.Funds[0].Denom, acct.State.Funds[0].Amount)) require.Equal(t, testutil.AkashDecCoin(t, rate.Amount.Int64()*ctx.BlockHeight()), acct.State.Transferred[0]) - payment, err := keeper.GetPayment(ctx, pid) + payment, err := ekeeper.GetPayment(ctx, pid) require.NoError(t, err) require.Equal(t, etypes.StateClosed, payment.State.State) @@ -154,20 +242,26 @@ func Test_PaymentCreate(t *testing.T) { ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 30) // can't withdraw from a closed payment - assert.Error(t, keeper.PaymentWithdraw(ctx, pid)) + assert.Error(t, ekeeper.PaymentWithdraw(ctx, pid)) // can't re-created a closed payment - assert.Error(t, keeper.PaymentCreate(ctx, pid, powner, sdk.NewDecCoinFromCoin(rate))) + assert.Error(t, ekeeper.PaymentCreate(ctx, pid, powner, sdk.NewDecCoinFromCoin(rate))) // closing the account transfers all remaining funds bkeeper. On("SendCoinsFromModuleToAccount", ctx, module.ModuleName, aowner, sdk.NewCoins(testutil.AkashCoin(t, amt.Amount.Int64()-rate.Amount.Int64()*30))). - Return(nil) - assert.NoError(t, keeper.AccountClose(ctx, aid)) + Return(nil).Once() + err = ekeeper.AccountClose(ctx, aid) + assert.NoError(t, err) } -func Test_Payment_Overdraw(t *testing.T) { - ctx, keeper, bkeeper := setupKeeper(t) +func Test_Overdraft(t *testing.T) { + ssuite := state.SetupTestSuite(t) + ctx := ssuite.Context() + + bkeeper := ssuite.BankKeeper() + ekeeper := ssuite.EscrowKeeper() + lid := testutil.LeaseID(t) did := lid.DeploymentID() @@ -179,11 +273,11 @@ func Test_Payment_Overdraw(t *testing.T) { powner := testutil.AccAddress(t) rate := testutil.AkashCoin(t, 10) - // create account + // create the account bkeeper. On("SendCoinsFromAccountToModule", ctx, aowner, module.ModuleName, sdk.NewCoins(amt)). - Return(nil) - err := keeper.AccountCreate(ctx, aid, aowner, []etypes.Depositor{{ + Return(nil).Once() + err := ekeeper.AccountCreate(ctx, aid, aowner, []etypes.Depositor{{ Owner: aowner.String(), Height: ctx.BlockHeight(), Balance: sdk.NewDecCoinFromCoin(amt), @@ -192,39 +286,114 @@ func Test_Payment_Overdraw(t *testing.T) { require.NoError(t, err) // create payment - err = keeper.PaymentCreate(ctx, pid, powner, sdk.NewDecCoinFromCoin(rate)) + err = ekeeper.PaymentCreate(ctx, pid, powner, sdk.NewDecCoinFromCoin(rate)) require.NoError(t, err) - // withdraw some funds + // withdraw after 105 blocks + // account is expected to be overdrafted for 50uakt, i.e. balance must show -50 blkdelta := int64(1000/10 + 5) ctx = ctx.WithBlockHeight(ctx.BlockHeight() + blkdelta) bkeeper. - On("SendCoinsFromModuleToAccount", ctx, module.ModuleName, powner, sdk.NewCoins(testutil.AkashCoin(t, 1000))). - Return(nil) + On("SendCoinsFromModuleToModule", mock.Anything, module.ModuleName, distrtypes.ModuleName, sdk.NewCoins(testutil.AkashCoin(t, 20))). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, module.ModuleName, powner, sdk.NewCoins(testutil.AkashCoin(t, 980))). + Return(nil).Once() - err = keeper.PaymentWithdraw(ctx, pid) + err = ekeeper.PaymentWithdraw(ctx, pid) require.NoError(t, err) - { - acct, err := keeper.GetAccount(ctx, aid) - require.NoError(t, err) - require.Equal(t, ctx.BlockHeight(), acct.State.SettledAt) + acct, err := ekeeper.GetAccount(ctx, aid) + require.NoError(t, err) + require.Equal(t, ctx.BlockHeight(), acct.State.SettledAt) - require.Equal(t, etypes.StateOverdrawn, acct.State.State) - require.True(t, acct.State.Funds[0].Amount.IsNegative()) - require.Equal(t, sdk.NewDecCoins(sdk.NewDecCoinFromCoin(amt)), acct.State.Transferred) + expectedOverdraft := sdkmath.LegacyNewDec(50) - payment, err := keeper.GetPayment(ctx, pid) - require.NoError(t, err) + require.Equal(t, etypes.StateOverdrawn, acct.State.State) + require.True(t, acct.State.Funds[0].Amount.IsNegative()) + require.Equal(t, sdk.NewDecCoins(sdk.NewDecCoinFromCoin(amt)), acct.State.Transferred) + require.Equal(t, expectedOverdraft, acct.State.Funds[0].Amount.Abs()) - require.Equal(t, etypes.StateOverdrawn, payment.State.State) - require.Equal(t, amt, payment.State.Withdrawn) - require.Equal(t, testutil.AkashDecCoin(t, 0), payment.State.Balance) - } + payment, err := ekeeper.GetPayment(ctx, pid) + require.NoError(t, err) + + require.Equal(t, etypes.StateOverdrawn, payment.State.State) + require.Equal(t, amt, payment.State.Withdrawn) + require.Equal(t, testutil.AkashDecCoin(t, 0), payment.State.Balance) + require.Equal(t, expectedOverdraft, payment.State.Unsettled.Amount) + + // account close will should not return an error when trying to close when overdrafted + // it will try to settle, as there were no deposits state must not change + err = ekeeper.AccountClose(ctx, aid) + assert.NoError(t, err) + + acct, err = ekeeper.GetAccount(ctx, aid) + require.NoError(t, err) + + require.Equal(t, etypes.StateOverdrawn, acct.State.State) + require.True(t, acct.State.Funds[0].Amount.IsNegative()) + require.Equal(t, sdk.NewDecCoins(sdk.NewDecCoinFromCoin(amt)), acct.State.Transferred) + require.Equal(t, expectedOverdraft, acct.State.Funds[0].Amount.Abs()) + + // attempting to close account 2nd time should not change the state + err = ekeeper.AccountClose(ctx, aid) + assert.NoError(t, err) + + acct, err = ekeeper.GetAccount(ctx, aid) + require.NoError(t, err) + + require.Equal(t, etypes.StateOverdrawn, acct.State.State) + require.True(t, acct.State.Funds[0].Amount.IsNegative()) + require.Equal(t, sdk.NewDecCoins(sdk.NewDecCoinFromCoin(amt)), acct.State.Transferred) + require.Equal(t, expectedOverdraft, acct.State.Funds[0].Amount.Abs()) + + payment, err = ekeeper.GetPayment(ctx, pid) + require.NoError(t, err) + + require.Equal(t, etypes.StateOverdrawn, payment.State.State) + require.Equal(t, amt, payment.State.Withdrawn) + require.Equal(t, testutil.AkashDecCoin(t, 0), payment.State.Balance) + + // deposit more funds into account + // this will trigger settlement and payoff if the deposit balance is sufficient + // 1st transfer: actual deposit of 1000uakt + // 2nd transfer: take rate 1uakt = 50 * 0.02 + // 3rd transfer: payment withdraw of 49uakt + // 4th transfer: return a remainder of 950uakt to the owner + bkeeper. + On("SendCoinsFromAccountToModule", ctx, aowner, module.ModuleName, sdk.NewCoins(amt)). + Return(nil).Once(). + On("SendCoinsFromModuleToModule", mock.Anything, module.ModuleName, distrtypes.ModuleName, sdk.NewCoins(testutil.AkashCoin(t, 1))). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, module.ModuleName, powner, sdk.NewCoins(testutil.AkashCoin(t, 49))). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, module.ModuleName, aowner, sdk.NewCoins(testutil.AkashCoin(t, 950))). + Return(nil).Once() + + err = ekeeper.AccountDeposit(ctx, aid, []etypes.Depositor{{ + Owner: aowner.String(), + Height: ctx.BlockHeight(), + Balance: sdk.NewDecCoinFromCoin(amt), + }}) + assert.NoError(t, err) + + acct, err = ekeeper.GetAccount(ctx, aid) + assert.NoError(t, err) + + require.Equal(t, etypes.StateClosed, acct.State.State) + require.Equal(t, acct.State.Funds[0].Amount, sdkmath.LegacyZeroDec()) + + payment, err = ekeeper.GetPayment(ctx, pid) + require.NoError(t, err) + require.Equal(t, etypes.StateClosed, payment.State.State) } func Test_PaymentCreate_later(t *testing.T) { - ctx, keeper, bkeeper := setupKeeper(t) + ssuite := state.SetupTestSuite(t) + ctx := ssuite.Context() + + bkeeper := ssuite.BankKeeper() + ekeeper := ssuite.EscrowKeeper() + lid := testutil.LeaseID(t) did := lid.DeploymentID() @@ -241,7 +410,7 @@ func Test_PaymentCreate_later(t *testing.T) { bkeeper. On("SendCoinsFromAccountToModule", ctx, aowner, module.ModuleName, sdk.NewCoins(amt)). Return(nil) - assert.NoError(t, keeper.AccountCreate(ctx, aid, aowner, []etypes.Depositor{{ + assert.NoError(t, ekeeper.AccountCreate(ctx, aid, aowner, []etypes.Depositor{{ Owner: aowner.String(), Height: ctx.BlockHeight(), Balance: sdk.NewDecCoinFromCoin(amt), @@ -251,17 +420,13 @@ func Test_PaymentCreate_later(t *testing.T) { ctx = ctx.WithBlockHeight(ctx.BlockHeight() + blkdelta) // create payment - assert.NoError(t, keeper.PaymentCreate(ctx, pid, powner, sdk.NewDecCoinFromCoin(rate))) + assert.NoError(t, ekeeper.PaymentCreate(ctx, pid, powner, sdk.NewDecCoinFromCoin(rate))) + + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) { - acct, err := keeper.GetAccount(ctx, aid) + acct, err := ekeeper.GetAccount(ctx, aid) require.NoError(t, err) - require.Equal(t, ctx.BlockHeight(), acct.State.SettledAt) + require.Equal(t, ctx.BlockHeight()-1, acct.State.SettledAt) } } - -func setupKeeper(t testing.TB) (sdk.Context, keeper.Keeper, *cmocks.BankKeeper) { - t.Helper() - ssuite := state.SetupTestSuite(t) - return ssuite.Context(), ssuite.EscrowKeeper(), ssuite.BankKeeper() -} diff --git a/x/market/handler/handler_test.go b/x/market/handler/handler_test.go index 8285dece1a..cc7641ee9d 100644 --- a/x/market/handler/handler_test.go +++ b/x/market/handler/handler_test.go @@ -6,17 +6,22 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/cometbft/cometbft/libs/rand" - sdkmath "cosmossdk.io/math" + "github.com/cometbft/cometbft/libs/rand" "github.com/cosmos/cosmos-sdk/baseapp" sdktestdata "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + dv1 "pkg.akt.dev/go/node/deployment/v1" dtypes "pkg.akt.dev/go/node/deployment/v1beta4" + emodule "pkg.akt.dev/go/node/escrow/module" + etypes "pkg.akt.dev/go/node/escrow/types/v1" + ev1 "pkg.akt.dev/go/node/escrow/v1" v1 "pkg.akt.dev/go/node/market/v1" types "pkg.akt.dev/go/node/market/v1beta5" ptypes "pkg.akt.dev/go/node/provider/v1beta4" @@ -25,13 +30,17 @@ import ( "pkg.akt.dev/go/testutil" "pkg.akt.dev/node/testutil/state" + dhandler "pkg.akt.dev/node/x/deployment/handler" + ehandler "pkg.akt.dev/node/x/escrow/handler" "pkg.akt.dev/node/x/market/handler" ) type testSuite struct { *state.TestSuite - handler baseapp.MsgServiceHandler - t testing.TB + handler baseapp.MsgServiceHandler + dhandler baseapp.MsgServiceHandler + ehandler baseapp.MsgServiceHandler + t testing.TB } func setupTestSuite(t *testing.T) *testSuite { @@ -49,6 +58,9 @@ func setupTestSuite(t *testing.T) *testSuite { }), } + suite.dhandler = dhandler.NewHandler(suite.DeploymentKeeper(), suite.MarketKeeper(), ssuite.EscrowKeeper()) + suite.ehandler = ehandler.NewHandler(suite.EscrowKeeper(), suite.AuthzKeeper(), suite.BankKeeper()) + return suite } @@ -61,6 +73,788 @@ func TestProviderBadMessageType(t *testing.T) { require.True(t, errors.Is(err, sdkerrors.ErrUnknownRequest)) } +func TestMarketFullFlowCloseDeployment(t *testing.T) { + defaultDeposit, err := dtypes.DefaultParams().MinDepositFor("uakt") + require.NoError(t, err) + + suite := setupTestSuite(t) + + ctx := suite.Context() + + deployment := testutil.Deployment(t) + group := testutil.DeploymentGroup(t, deployment.ID, 0) + group.GroupSpec.Resources = testutil.Resources(t) + + owner := sdk.MustAccAddressFromBech32(deployment.ID.Owner) + + // we can create provider via keeper in this test + provider := suite.createProvider(group.GroupSpec.Requirements.Attributes).Owner + providerAddr, err := sdk.AccAddressFromBech32(provider) + require.NoError(t, err) + + escrowBalance := sdk.NewCoins(sdk.NewInt64Coin("uakt", 0)) + distrBalance := sdk.NewCoins(sdk.NewInt64Coin("uakt", 0)) + + dmsg := &dtypes.MsgCreateDeployment{ + ID: deployment.ID, + Groups: dtypes.GroupSpecs{group.GroupSpec}, + Deposit: deposit.Deposit{ + Amount: defaultDeposit, + Sources: deposit.Sources{deposit.SourceBalance}, + }, + } + + balances := map[string]sdk.Coin{ + deployment.ID.Owner: sdk.NewInt64Coin("uakt", 10000000), + provider: sdk.NewInt64Coin("uakt", 10000000), + } + + sendCoinsFromAccountToModule := func(args mock.Arguments) { + addr := args[1].(sdk.AccAddress) + module := args[2].(string) + amount := args[3].(sdk.Coins) + + require.Len(t, amount, 1) + + balances[addr.String()] = balances[addr.String()].Sub(amount[0]) + switch module { + case emodule.ModuleName: + escrowBalance = escrowBalance.Add(amount...) + default: + t.Fatalf("unexpected send to module %s", module) + } + } + + sendCoinsFromModuleToAccount := func(args mock.Arguments) { + module := args[1].(string) + addr := args[2].(sdk.AccAddress) + amount := args[3].(sdk.Coins) + + require.Len(t, amount, 1) + + balances[addr.String()] = balances[addr.String()].Add(amount[0]) + + switch module { + case emodule.ModuleName: + escrowBalance = escrowBalance.Sub(amount...) + default: + t.Fatalf("unexpected send from module %s", module) + } + } + + sendCoinsFromModuleToModule := func(args mock.Arguments) { + from := args[1].(string) + to := args[2].(string) + amount := args[3].(sdk.Coins) + + require.Equal(t, emodule.ModuleName, from) + require.Equal(t, distrtypes.ModuleName, to) + require.Len(t, amount, 1) + + distrBalance = distrBalance.Add(amount...) + escrowBalance = escrowBalance.Sub(amount...) + } + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + bkeeper. + On("SpendableCoin", mock.Anything, mock.Anything, mock.Anything). + Return(func(args mock.Arguments) sdk.Coin { + addr := args[1].(sdk.AccAddress) + denom := args[2].(string) + + require.Equal(t, "uakt", denom) + + return balances[addr.String()] + }) + }) + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(sendCoinsFromAccountToModule).Return(nil).Once() + }) + res, err := suite.dhandler(ctx, dmsg) + require.NoError(t, err) + require.NotNil(t, res) + + order, found := suite.MarketKeeper().GetOrder(ctx, v1.OrderID{ + Owner: deployment.ID.Owner, + DSeq: deployment.ID.DSeq, + GSeq: 1, + OSeq: 1, + }) + + require.True(t, found) + + bmsg := &types.MsgCreateBid{ + ID: v1.MakeBidID(order.ID, providerAddr), + Price: sdk.NewDecCoin(testutil.CoinDenom, sdkmath.NewInt(1)), + Deposit: deposit.Deposit{ + Amount: types.DefaultBidMinDeposit, + Sources: deposit.Sources{deposit.SourceBalance}, + }, + } + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(sendCoinsFromAccountToModule).Return(nil).Once() + }) + + res, err = suite.handler(ctx, bmsg) + require.NotNil(t, res) + require.NoError(t, err) + + bid := v1.MakeBidID(order.ID, providerAddr) + + t.Run("ensure bid event created", func(t *testing.T) { + iev, err := sdk.ParseTypedEvent(res.Events[3]) + require.NoError(t, err) + require.IsType(t, &v1.EventBidCreated{}, iev) + + dev := iev.(*v1.EventBidCreated) + + require.Equal(t, bid, dev.ID) + }) + + _, found = suite.MarketKeeper().GetBid(ctx, bid) + require.True(t, found) + + lmsg := &types.MsgCreateLease{ + BidID: bid, + } + + lid := v1.MakeLeaseID(bid) + res, err = suite.handler(ctx, lmsg) + require.NotNil(t, res) + require.NoError(t, err) + + t.Run("ensure lease event created", func(t *testing.T) { + iev, err := sdk.ParseTypedEvent(res.Events[4]) + require.NoError(t, err) + require.IsType(t, &v1.EventLeaseCreated{}, iev) + + dev := iev.(*v1.EventLeaseCreated) + + require.Equal(t, lid, dev.ID) + }) + + // find just created escrow account + eacc, err := suite.EscrowKeeper().GetAccount(ctx, deployment.ID.ToEscrowAccountID()) + require.NoError(t, err) + require.NotNil(t, eacc) + + // find just created escrow payment + epmnt, err := suite.EscrowKeeper().GetPayment(ctx, lid.ToEscrowPaymentID()) + require.NoError(t, err) + require.NotNil(t, epmnt) + + blocks := eacc.State.Funds[0].Amount.Quo(epmnt.State.Rate.Amount) + + ctx = ctx.WithBlockHeight(blocks.TruncateInt64() + 100) + + dcmsg := &dtypes.MsgCloseDeployment{ + ID: deployment.ID, + } + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(sendCoinsFromModuleToModule).Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(sendCoinsFromModuleToAccount).Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(sendCoinsFromModuleToAccount).Return(nil).Once() + }) + + // this will trigger settlement and payoff if the deposit balance is sufficient + // 1nd transfer: take rate 10000uakt = 500,000 * 0.02 + // 2nd transfer: returned bid deposit back to the provider + // 3rd transfer: payment withdraw of 490,000uakt + res, err = suite.dhandler(ctx, dcmsg) + require.NoError(t, err) + require.NotNil(t, res) + + eacc, err = suite.EscrowKeeper().GetAccount(ctx, deployment.ID.ToEscrowAccountID()) + require.NoError(t, err) + require.NotNil(t, eacc) + + // find just created escrow payment + epmnt, err = suite.EscrowKeeper().GetPayment(ctx, lid.ToEscrowPaymentID()) + require.NoError(t, err) + require.NotNil(t, epmnt) + + // both escrow account and payment are expected to be in overdrawn state + require.Equal(t, etypes.StateOverdrawn, eacc.State.State) + require.Equal(t, etypes.StateOverdrawn, epmnt.State.State) + + expectedOverdraft := epmnt.State.Rate.Amount.MulInt64(100) + require.True(t, eacc.State.Funds[0].Amount.IsNegative()) + require.Equal(t, expectedOverdraft, eacc.State.Funds[0].Amount.Abs()) + require.Equal(t, expectedOverdraft, epmnt.State.Unsettled.Amount) + + // lease must be in closed state + lease, found := suite.MarketKeeper().GetLease(ctx, lid) + require.True(t, found) + require.Equal(t, v1.LeaseClosed, lease.State) + + // lease must be in closed state + bidObj, found := suite.MarketKeeper().GetBid(ctx, bid) + require.True(t, found) + require.Equal(t, types.BidClosed, bidObj.State) + + // deployment must be in closed state + depl, found := suite.DeploymentKeeper().GetDeployment(ctx, lid.DeploymentID()) + require.True(t, found) + require.Equal(t, dv1.DeploymentClosed, depl.State) + + // should not be able to close escrow account in overdrawn state + err = suite.EscrowKeeper().AccountClose(ctx, deployment.ID.ToEscrowAccountID()) + require.NoError(t, err) + + // both account and payment should remain in overdrawn state + eacc, err = suite.EscrowKeeper().GetAccount(ctx, deployment.ID.ToEscrowAccountID()) + require.NoError(t, err) + require.NotNil(t, eacc) + + // find just created escrow payment + epmnt, err = suite.EscrowKeeper().GetPayment(ctx, lid.ToEscrowPaymentID()) + require.NoError(t, err) + require.NotNil(t, epmnt) + + // both escrow account and payment are expected to be in overdrawn state + require.Equal(t, etypes.StateOverdrawn, eacc.State.State) + require.Equal(t, etypes.StateOverdrawn, epmnt.State.State) + + require.Equal(t, expectedOverdraft, eacc.State.Funds[0].Amount.Abs()) + require.Equal(t, expectedOverdraft, epmnt.State.Unsettled.Amount) + + depositMsg := &ev1.MsgAccountDeposit{ + Signer: owner.String(), + ID: deployment.ID.ToEscrowAccountID(), + Deposit: deposit.Deposit{ + Amount: sdk.NewCoin(defaultDeposit.Denom, expectedOverdraft.TruncateInt()), + Sources: deposit.Sources{deposit.SourceBalance}, + }, + } + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(sendCoinsFromModuleToModule).Return(nil).Once(). + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, emodule.ModuleName, mock.Anything).Run(sendCoinsFromAccountToModule).Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(sendCoinsFromModuleToAccount).Return(nil).Once() + }) + + res, err = suite.ehandler(ctx, depositMsg) + require.NoError(t, err) + require.NotNil(t, res) + + // after deposit into an overdrawn account, account and payments should be settled and closed (if sufficient balance is provided) + eacc, err = suite.EscrowKeeper().GetAccount(ctx, deployment.ID.ToEscrowAccountID()) + require.NoError(t, err) + require.NotNil(t, eacc) + + // find just created escrow payment + epmnt, err = suite.EscrowKeeper().GetPayment(ctx, lid.ToEscrowPaymentID()) + require.NoError(t, err) + require.NotNil(t, epmnt) + + // both escrow account and payment are expected to be in overdrawn state + require.Equal(t, etypes.StateClosed, eacc.State.State) + require.Equal(t, etypes.StateClosed, epmnt.State.State) + + require.True(t, eacc.State.Funds[0].Amount.IsZero()) + require.True(t, epmnt.State.Unsettled.Amount.IsZero()) + + // at the end of the test module escrow account should be 0 + require.Equal(t, sdk.NewCoins(sdk.NewInt64Coin("uakt", 0)), escrowBalance) + + // at the end of the test module distribution account should be 10002uakt + require.Equal(t, sdk.NewCoins(sdk.NewInt64Coin("uakt", 10002)), distrBalance) + + // at the end of the test provider account should be 10490098uakt + require.Equal(t, sdk.NewInt64Coin("uakt", 10490098), balances[provider]) + + // at the end of the test owner account should be 9499900uakt + require.Equal(t, sdk.NewInt64Coin("uakt", 9499900), balances[owner.String()]) +} + +func TestMarketFullFlowCloseLease(t *testing.T) { + defaultDeposit, err := dtypes.DefaultParams().MinDepositFor("uakt") + require.NoError(t, err) + + suite := setupTestSuite(t) + + ctx := suite.Context() + + deployment := testutil.Deployment(t) + group := testutil.DeploymentGroup(t, deployment.ID, 0) + group.GroupSpec.Resources = testutil.Resources(t) + + owner := sdk.MustAccAddressFromBech32(deployment.ID.Owner) + + dmsg := &dtypes.MsgCreateDeployment{ + ID: deployment.ID, + Groups: dtypes.GroupSpecs{group.GroupSpec}, + Deposit: deposit.Deposit{ + Amount: defaultDeposit, + Sources: deposit.Sources{deposit.SourceBalance}, + }, + } + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, owner, emodule.ModuleName, sdk.Coins{dmsg.Deposit.Amount}). + Return(nil).Once() + }) + + res, err := suite.dhandler(ctx, dmsg) + require.NoError(t, err) + require.NotNil(t, res) + + order, found := suite.MarketKeeper().GetOrder(ctx, v1.OrderID{ + Owner: deployment.ID.Owner, + DSeq: deployment.ID.DSeq, + GSeq: 1, + OSeq: 1, + }) + + require.True(t, found) + + // we can create provider via keeper in this test + provider := suite.createProvider(group.GroupSpec.Requirements.Attributes).Owner + providerAddr, err := sdk.AccAddressFromBech32(provider) + require.NoError(t, err) + + bmsg := &types.MsgCreateBid{ + ID: v1.MakeBidID(order.ID, providerAddr), + Price: sdk.NewDecCoin(testutil.CoinDenom, sdkmath.NewInt(1)), + Deposit: deposit.Deposit{ + Amount: types.DefaultBidMinDeposit, + Sources: deposit.Sources{deposit.SourceBalance}, + }, + } + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, providerAddr, emodule.ModuleName, sdk.Coins{types.DefaultBidMinDeposit}). + Return(nil).Once() + }) + res, err = suite.handler(ctx, bmsg) + require.NotNil(t, res) + require.NoError(t, err) + + bid := v1.MakeBidID(order.ID, providerAddr) + + t.Run("ensure bid event created", func(t *testing.T) { + iev, err := sdk.ParseTypedEvent(res.Events[3]) + require.NoError(t, err) + require.IsType(t, &v1.EventBidCreated{}, iev) + + dev := iev.(*v1.EventBidCreated) + + require.Equal(t, bid, dev.ID) + }) + + _, found = suite.MarketKeeper().GetBid(ctx, bid) + require.True(t, found) + + lmsg := &types.MsgCreateLease{ + BidID: bid, + } + + lid := v1.MakeLeaseID(bid) + res, err = suite.handler(ctx, lmsg) + require.NotNil(t, res) + require.NoError(t, err) + + t.Run("ensure lease event created", func(t *testing.T) { + iev, err := sdk.ParseTypedEvent(res.Events[4]) + require.NoError(t, err) + require.IsType(t, &v1.EventLeaseCreated{}, iev) + + dev := iev.(*v1.EventLeaseCreated) + + require.Equal(t, lid, dev.ID) + }) + + // find just created escrow account + eacc, err := suite.EscrowKeeper().GetAccount(ctx, deployment.ID.ToEscrowAccountID()) + require.NoError(t, err) + require.NotNil(t, eacc) + + // find just created escrow payment + epmnt, err := suite.EscrowKeeper().GetPayment(ctx, lid.ToEscrowPaymentID()) + require.NoError(t, err) + require.NotNil(t, epmnt) + + blocks := eacc.State.Funds[0].Amount.Quo(epmnt.State.Rate.Amount) + + ctx = ctx.WithBlockHeight(blocks.TruncateInt64() + 100) + + dcmsg := &types.MsgCloseLease{ + ID: lid, + } + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + // this will trigger settlement and payoff if the deposit balance is sufficient + // 1nd transfer: take rate 10000uakt = 500,000 * 0.02 + // 2nd transfer: returned bid deposit back to the provider + // 3rd transfer: payment withdraw of 490,000uakt + takeRate := sdkmath.LegacyNewDecFromInt(defaultDeposit.Amount) + takeRate.MulMut(sdkmath.LegacyMustNewDecFromStr("0.02")) + + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, emodule.ModuleName, distrtypes.ModuleName, sdk.Coins{sdk.NewCoin(defaultDeposit.Denom, takeRate.TruncateInt())}). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, emodule.ModuleName, providerAddr, sdk.NewCoins(testutil.AkashCoin(t, 500_000))). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, emodule.ModuleName, providerAddr, sdk.NewCoins(testutil.AkashCoin(t, 490_000))). + Return(nil).Once() + }) + + res, err = suite.handler(ctx, dcmsg) + require.NoError(t, err) + require.NotNil(t, res) + + eacc, err = suite.EscrowKeeper().GetAccount(ctx, deployment.ID.ToEscrowAccountID()) + require.NoError(t, err) + require.NotNil(t, eacc) + + // find just created escrow payment + epmnt, err = suite.EscrowKeeper().GetPayment(ctx, lid.ToEscrowPaymentID()) + require.NoError(t, err) + require.NotNil(t, epmnt) + + // both escrow account and payment are expected to be in overdrawn state + require.Equal(t, etypes.StateOverdrawn, eacc.State.State) + require.Equal(t, etypes.StateOverdrawn, epmnt.State.State) + + expectedOverdraft := epmnt.State.Rate.Amount.MulInt64(100) + require.True(t, eacc.State.Funds[0].Amount.IsNegative()) + require.Equal(t, expectedOverdraft, eacc.State.Funds[0].Amount.Abs()) + require.Equal(t, expectedOverdraft, epmnt.State.Unsettled.Amount) + + // lease must be in closed state + lease, found := suite.MarketKeeper().GetLease(ctx, lid) + require.True(t, found) + require.Equal(t, v1.LeaseClosed, lease.State) + + // lease must be in closed state + bidObj, found := suite.MarketKeeper().GetBid(ctx, bid) + require.True(t, found) + require.Equal(t, types.BidClosed, bidObj.State) + + // deployment must be in closed state + depl, found := suite.DeploymentKeeper().GetDeployment(ctx, lid.DeploymentID()) + require.True(t, found) + require.Equal(t, dv1.DeploymentClosed, depl.State) + + // should not be able to close escrow account in overdrawn state + err = suite.EscrowKeeper().AccountClose(ctx, deployment.ID.ToEscrowAccountID()) + require.NoError(t, err) + + // both account and payment should remain in overdrawn state + eacc, err = suite.EscrowKeeper().GetAccount(ctx, deployment.ID.ToEscrowAccountID()) + require.NoError(t, err) + require.NotNil(t, eacc) + + // find just created escrow payment + epmnt, err = suite.EscrowKeeper().GetPayment(ctx, lid.ToEscrowPaymentID()) + require.NoError(t, err) + require.NotNil(t, epmnt) + + // both escrow account and payment are expected to be in overdrawn state + require.Equal(t, etypes.StateOverdrawn, eacc.State.State) + require.Equal(t, etypes.StateOverdrawn, epmnt.State.State) + + require.Equal(t, expectedOverdraft, eacc.State.Funds[0].Amount.Abs()) + require.Equal(t, expectedOverdraft, epmnt.State.Unsettled.Amount) + + depositMsg := &ev1.MsgAccountDeposit{ + Signer: owner.String(), + ID: deployment.ID.ToEscrowAccountID(), + Deposit: deposit.Deposit{ + Amount: sdk.NewCoin(defaultDeposit.Denom, expectedOverdraft.TruncateInt()), + Sources: deposit.Sources{deposit.SourceBalance}, + }, + } + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, owner, emodule.ModuleName, sdk.Coins{depositMsg.Deposit.Amount}). + Return(nil).Once(). + On("SendCoinsFromModuleToModule", mock.Anything, emodule.ModuleName, distrtypes.ModuleName, sdk.Coins{sdk.NewInt64Coin(depositMsg.Deposit.Amount.Denom, 2)}). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, emodule.ModuleName, providerAddr, sdk.NewCoins(testutil.AkashCoin(t, 98))). + Return(nil).Once() + }) + + res, err = suite.ehandler(ctx, depositMsg) + require.NoError(t, err) + require.NotNil(t, res) + + // after deposit into overdrawn account, account and payments should be settled and closed (if sufficient balance is provided) + eacc, err = suite.EscrowKeeper().GetAccount(ctx, deployment.ID.ToEscrowAccountID()) + require.NoError(t, err) + require.NotNil(t, eacc) + + // find just created escrow payment + epmnt, err = suite.EscrowKeeper().GetPayment(ctx, lid.ToEscrowPaymentID()) + require.NoError(t, err) + require.NotNil(t, epmnt) + + // both escrow account and payment are expected to be in overdrawn state + require.Equal(t, etypes.StateClosed, eacc.State.State) + require.Equal(t, etypes.StateClosed, epmnt.State.State) + + require.True(t, eacc.State.Funds[0].Amount.IsZero()) + require.True(t, epmnt.State.Unsettled.Amount.IsZero()) +} + +func TestMarketFullFlowCloseBid(t *testing.T) { + defaultDeposit, err := dtypes.DefaultParams().MinDepositFor("uakt") + require.NoError(t, err) + + suite := setupTestSuite(t) + + ctx := suite.Context() + + deployment := testutil.Deployment(t) + group := testutil.DeploymentGroup(t, deployment.ID, 0) + group.GroupSpec.Resources = testutil.Resources(t) + + owner := sdk.MustAccAddressFromBech32(deployment.ID.Owner) + + dmsg := &dtypes.MsgCreateDeployment{ + ID: deployment.ID, + Groups: dtypes.GroupSpecs{group.GroupSpec}, + Deposit: deposit.Deposit{ + Amount: defaultDeposit, + Sources: deposit.Sources{deposit.SourceBalance}, + }, + } + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, owner, emodule.ModuleName, sdk.Coins{dmsg.Deposit.Amount}). + Return(nil).Once() + }) + + res, err := suite.dhandler(ctx, dmsg) + require.NoError(t, err) + require.NotNil(t, res) + + order, found := suite.MarketKeeper().GetOrder(ctx, v1.OrderID{ + Owner: deployment.ID.Owner, + DSeq: deployment.ID.DSeq, + GSeq: 1, + OSeq: 1, + }) + + require.True(t, found) + + // we can create provider via keeper in this test + provider := suite.createProvider(group.GroupSpec.Requirements.Attributes).Owner + providerAddr, err := sdk.AccAddressFromBech32(provider) + require.NoError(t, err) + + bmsg := &types.MsgCreateBid{ + ID: v1.MakeBidID(order.ID, providerAddr), + Price: sdk.NewDecCoin(testutil.CoinDenom, sdkmath.NewInt(1)), + Deposit: deposit.Deposit{ + Amount: types.DefaultBidMinDeposit, + Sources: deposit.Sources{deposit.SourceBalance}, + }, + } + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, providerAddr, emodule.ModuleName, sdk.Coins{types.DefaultBidMinDeposit}). + Return(nil).Once() + }) + res, err = suite.handler(ctx, bmsg) + require.NotNil(t, res) + require.NoError(t, err) + + bid := v1.MakeBidID(order.ID, providerAddr) + + t.Run("ensure bid event created", func(t *testing.T) { + iev, err := sdk.ParseTypedEvent(res.Events[3]) + require.NoError(t, err) + require.IsType(t, &v1.EventBidCreated{}, iev) + + dev := iev.(*v1.EventBidCreated) + + require.Equal(t, bid, dev.ID) + }) + + _, found = suite.MarketKeeper().GetBid(ctx, bid) + require.True(t, found) + + lmsg := &types.MsgCreateLease{ + BidID: bid, + } + + lid := v1.MakeLeaseID(bid) + res, err = suite.handler(ctx, lmsg) + require.NotNil(t, res) + require.NoError(t, err) + + t.Run("ensure lease event created", func(t *testing.T) { + iev, err := sdk.ParseTypedEvent(res.Events[4]) + require.NoError(t, err) + require.IsType(t, &v1.EventLeaseCreated{}, iev) + + dev := iev.(*v1.EventLeaseCreated) + + require.Equal(t, lid, dev.ID) + }) + + // find just created escrow account + eacc, err := suite.EscrowKeeper().GetAccount(ctx, deployment.ID.ToEscrowAccountID()) + require.NoError(t, err) + require.NotNil(t, eacc) + + // find just created escrow payment + epmnt, err := suite.EscrowKeeper().GetPayment(ctx, lid.ToEscrowPaymentID()) + require.NoError(t, err) + require.NotNil(t, epmnt) + + blocks := eacc.State.Funds[0].Amount.Quo(epmnt.State.Rate.Amount) + + ctx = ctx.WithBlockHeight(blocks.TruncateInt64() + 100) + + dcmsg := &types.MsgCloseBid{ + ID: bid, + } + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + // this will trigger settlement and payoff if the deposit balance is sufficient + // 1nd transfer: take rate 10000uakt = 500,000 * 0.02 + // 2nd transfer: returned bid deposit back to the provider + // 3rd transfer: payment withdraw of 490,000uakt + takeRate := sdkmath.LegacyNewDecFromInt(defaultDeposit.Amount) + takeRate.MulMut(sdkmath.LegacyMustNewDecFromStr("0.02")) + + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, emodule.ModuleName, distrtypes.ModuleName, sdk.Coins{sdk.NewCoin(defaultDeposit.Denom, takeRate.TruncateInt())}). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, emodule.ModuleName, providerAddr, sdk.NewCoins(testutil.AkashCoin(t, 500_000))). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, emodule.ModuleName, providerAddr, sdk.NewCoins(testutil.AkashCoin(t, 490_000))). + Return(nil).Once() + }) + + res, err = suite.handler(ctx, dcmsg) + require.NoError(t, err) + require.NotNil(t, res) + + eacc, err = suite.EscrowKeeper().GetAccount(ctx, deployment.ID.ToEscrowAccountID()) + require.NoError(t, err) + require.NotNil(t, eacc) + + // find just created escrow payment + epmnt, err = suite.EscrowKeeper().GetPayment(ctx, lid.ToEscrowPaymentID()) + require.NoError(t, err) + require.NotNil(t, epmnt) + + // both escrow account and payment are expected to be in overdrawn state + require.Equal(t, etypes.StateOverdrawn, eacc.State.State) + require.Equal(t, etypes.StateOverdrawn, epmnt.State.State) + + expectedOverdraft := epmnt.State.Rate.Amount.MulInt64(100) + require.True(t, eacc.State.Funds[0].Amount.IsNegative()) + require.Equal(t, expectedOverdraft, eacc.State.Funds[0].Amount.Abs()) + require.Equal(t, expectedOverdraft, epmnt.State.Unsettled.Amount) + + // lease must be in closed state + lease, found := suite.MarketKeeper().GetLease(ctx, lid) + require.True(t, found) + require.Equal(t, v1.LeaseClosed, lease.State) + + // lease must be in closed state + bidObj, found := suite.MarketKeeper().GetBid(ctx, bid) + require.True(t, found) + require.Equal(t, types.BidClosed, bidObj.State) + + // deployment must be in closed state + depl, found := suite.DeploymentKeeper().GetDeployment(ctx, lid.DeploymentID()) + require.True(t, found) + require.Equal(t, dv1.DeploymentClosed, depl.State) + + // should not be able to close escrow account in overdrawn state + err = suite.EscrowKeeper().AccountClose(ctx, deployment.ID.ToEscrowAccountID()) + require.NoError(t, err) + + // both account and payment should remain in overdrawn state + eacc, err = suite.EscrowKeeper().GetAccount(ctx, deployment.ID.ToEscrowAccountID()) + require.NoError(t, err) + require.NotNil(t, eacc) + + // find just created escrow payment + epmnt, err = suite.EscrowKeeper().GetPayment(ctx, lid.ToEscrowPaymentID()) + require.NoError(t, err) + require.NotNil(t, epmnt) + + // both escrow account and payment are expected to be in overdrawn state + require.Equal(t, etypes.StateOverdrawn, eacc.State.State) + require.Equal(t, etypes.StateOverdrawn, epmnt.State.State) + + require.Equal(t, expectedOverdraft, eacc.State.Funds[0].Amount.Abs()) + require.Equal(t, expectedOverdraft, epmnt.State.Unsettled.Amount) + + depositMsg := &ev1.MsgAccountDeposit{ + Signer: owner.String(), + ID: deployment.ID.ToEscrowAccountID(), + Deposit: deposit.Deposit{ + Amount: sdk.NewCoin(defaultDeposit.Denom, expectedOverdraft.TruncateInt()), + Sources: deposit.Sources{deposit.SourceBalance}, + }, + } + + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, owner, emodule.ModuleName, sdk.Coins{depositMsg.Deposit.Amount}). + Return(nil).Once(). + On("SendCoinsFromModuleToModule", mock.Anything, emodule.ModuleName, distrtypes.ModuleName, sdk.Coins{sdk.NewInt64Coin(depositMsg.Deposit.Amount.Denom, 2)}). + Return(nil).Once(). + On("SendCoinsFromModuleToAccount", mock.Anything, emodule.ModuleName, providerAddr, sdk.NewCoins(testutil.AkashCoin(t, 98))). + Return(nil).Once() + }) + + res, err = suite.ehandler(ctx, depositMsg) + require.NoError(t, err) + require.NotNil(t, res) + + // after deposit into overdrawn account, account and payments should be settled and closed (if sufficient balance is provided) + eacc, err = suite.EscrowKeeper().GetAccount(ctx, deployment.ID.ToEscrowAccountID()) + require.NoError(t, err) + require.NotNil(t, eacc) + + // find just created escrow payment + epmnt, err = suite.EscrowKeeper().GetPayment(ctx, lid.ToEscrowPaymentID()) + require.NoError(t, err) + require.NotNil(t, epmnt) + + // both escrow account and payment are expected to be in overdrawn state + require.Equal(t, etypes.StateClosed, eacc.State.State) + require.Equal(t, etypes.StateClosed, epmnt.State.State) + + require.True(t, eacc.State.Funds[0].Amount.IsZero()) + require.True(t, epmnt.State.Unsettled.Amount.IsZero()) +} + func TestCreateBidValid(t *testing.T) { suite := setupTestSuite(t) @@ -79,6 +873,20 @@ func TestCreateBidValid(t *testing.T) { }, } + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) + res, err := suite.handler(suite.Context(), msg) require.NotNil(t, res) require.NoError(t, err) @@ -102,6 +910,19 @@ func TestCreateBidValid(t *testing.T) { func TestCreateBidInvalidPrice(t *testing.T) { suite := setupTestSuite(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) order, gspec := suite.createOrder(nil) @@ -141,6 +962,19 @@ func TestCreateBidNonExistingOrder(t *testing.T) { func TestCreateBidClosedOrder(t *testing.T) { suite := setupTestSuite(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) order, gspec := suite.createOrder(nil) provider := suite.createProvider(gspec.Requirements.Attributes).Owner @@ -162,6 +996,19 @@ func TestCreateBidClosedOrder(t *testing.T) { func TestCreateBidOverprice(t *testing.T) { suite := setupTestSuite(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) resources := dtypes.ResourceUnits{ { @@ -185,6 +1032,19 @@ func TestCreateBidOverprice(t *testing.T) { func TestCreateBidInvalidProvider(t *testing.T) { suite := setupTestSuite(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) order, _ := suite.createOrder(testutil.Resources(t)) @@ -200,6 +1060,19 @@ func TestCreateBidInvalidProvider(t *testing.T) { func TestCreateBidInvalidAttributes(t *testing.T) { suite := setupTestSuite(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) order, _ := suite.createOrder(testutil.Resources(t)) providerAddr, err := sdk.AccAddressFromBech32(suite.createProvider(nil).Owner) @@ -218,6 +1091,20 @@ func TestCreateBidInvalidAttributes(t *testing.T) { func TestCreateBidAlreadyExists(t *testing.T) { suite := setupTestSuite(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) + order, gspec := suite.createOrder(testutil.Resources(t)) provider := suite.createProvider(gspec.Requirements.Attributes).Owner providerAddr, err := sdk.AccAddressFromBech32(provider) @@ -296,6 +1183,19 @@ func TestCloseOrderValid(t *testing.T) { func TestCloseBidNonExisting(t *testing.T) { suite := setupTestSuite(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) order, gspec := suite.createOrder(testutil.Resources(t)) @@ -315,6 +1215,19 @@ func TestCloseBidNonExisting(t *testing.T) { func TestCloseBidUnknownLease(t *testing.T) { suite := setupTestSuite(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) bid, _ := suite.createBid() @@ -331,6 +1244,19 @@ func TestCloseBidUnknownLease(t *testing.T) { func TestCloseBidValid(t *testing.T) { suite := setupTestSuite(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) _, bid, _ := suite.createLease() @@ -357,6 +1283,19 @@ func TestCloseBidValid(t *testing.T) { func TestCloseBidWithStateOpen(t *testing.T) { suite := setupTestSuite(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) bid, _ := suite.createBid() @@ -488,3 +1427,26 @@ func (st *testSuite) createProvider(attr attr.Attributes) ptypes.Provider { return prov } + +func (st *testSuite) createDeployment() (dv1.Deployment, dtypes.Groups) { + st.t.Helper() + + deployment := testutil.Deployment(st.t) + group := testutil.DeploymentGroup(st.t, deployment.ID, 0) + group.GroupSpec.Resources = dtypes.ResourceUnits{ + { + Resources: testutil.ResourceUnits(st.t), + Count: 1, + Price: testutil.AkashDecCoinRandom(st.t), + }, + } + groups := dtypes.Groups{ + group, + } + + for i := range groups { + groups[i].State = dtypes.GroupOpen + } + + return deployment, groups +} diff --git a/x/market/hooks/hooks.go b/x/market/hooks/hooks.go index 212880f30a..846252e69a 100644 --- a/x/market/hooks/hooks.go +++ b/x/market/hooks/hooks.go @@ -11,8 +11,8 @@ import ( ) type Hooks interface { - OnEscrowAccountClosed(ctx sdk.Context, obj etypes.Account) - OnEscrowPaymentClosed(ctx sdk.Context, obj etypes.Payment) + OnEscrowAccountClosed(ctx sdk.Context, obj etypes.Account) error + OnEscrowPaymentClosed(ctx sdk.Context, obj etypes.Payment) error } type hooks struct { @@ -27,21 +27,24 @@ func New(dkeeper DeploymentKeeper, mkeeper MarketKeeper) Hooks { } } -func (h *hooks) OnEscrowAccountClosed(ctx sdk.Context, obj etypes.Account) { +func (h *hooks) OnEscrowAccountClosed(ctx sdk.Context, obj etypes.Account) error { id, err := dv1.DeploymentIDFromEscrowID(obj.ID) if err != nil { - return + return err } deployment, found := h.dkeeper.GetDeployment(ctx, id) if !found { - return + return nil } if deployment.State != dv1.DeploymentActive { - return + return nil + } + err = h.dkeeper.CloseDeployment(ctx, deployment) + if err != nil { + return err } - _ = h.dkeeper.CloseDeployment(ctx, deployment) gstate := dtypes.GroupClosed if obj.State.State == etypes.StateOverdrawn { @@ -50,43 +53,65 @@ func (h *hooks) OnEscrowAccountClosed(ctx sdk.Context, obj etypes.Account) { for _, group := range h.dkeeper.GetGroups(ctx, deployment.ID) { if group.ValidateClosable() == nil { - _ = h.dkeeper.OnCloseGroup(ctx, group, gstate) - _ = h.mkeeper.OnGroupClosed(ctx, group.ID) + err = h.dkeeper.OnCloseGroup(ctx, group, gstate) + if err != nil { + return err + } + err = h.mkeeper.OnGroupClosed(ctx, group.ID) + if err != nil { + return err + } } } + + return nil } -func (h *hooks) OnEscrowPaymentClosed(ctx sdk.Context, obj etypes.Payment) { +func (h *hooks) OnEscrowPaymentClosed(ctx sdk.Context, obj etypes.Payment) error { id, err := mv1.LeaseIDFromPaymentID(obj.ID) if err != nil { - return + return nil } bid, ok := h.mkeeper.GetBid(ctx, id.BidID()) if !ok { - return + return nil } if bid.State != mtypes.BidActive { - return + return nil } order, ok := h.mkeeper.GetOrder(ctx, id.OrderID()) if !ok { - return + return mv1.ErrOrderNotFound } lease, ok := h.mkeeper.GetLease(ctx, id) if !ok { - return + return mv1.ErrLeaseNotFound } - _ = h.mkeeper.OnOrderClosed(ctx, order) - _ = h.mkeeper.OnBidClosed(ctx, bid) + err = h.mkeeper.OnOrderClosed(ctx, order) + if err != nil { + return err + } + err = h.mkeeper.OnBidClosed(ctx, bid) + if err != nil { + return err + } if obj.State.State == etypes.StateOverdrawn { - _ = h.mkeeper.OnLeaseClosed(ctx, lease, mv1.LeaseInsufficientFunds, mv1.LeaseClosedReasonInsufficientFunds) + err = h.mkeeper.OnLeaseClosed(ctx, lease, mv1.LeaseInsufficientFunds, mv1.LeaseClosedReasonInsufficientFunds) + if err != nil { + return err + } } else { - _ = h.mkeeper.OnLeaseClosed(ctx, lease, mv1.LeaseClosed, mv1.LeaseClosedReasonUnspecified) + err = h.mkeeper.OnLeaseClosed(ctx, lease, mv1.LeaseClosed, mv1.LeaseClosedReasonUnspecified) + if err != nil { + return err + } } + + return nil } diff --git a/x/market/keeper/grpc_query_test.go b/x/market/keeper/grpc_query_test.go index 3b1bfa08a2..fc544c7ef5 100644 --- a/x/market/keeper/grpc_query_test.go +++ b/x/market/keeper/grpc_query_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/cosmos/cosmos-sdk/baseapp" @@ -416,6 +417,20 @@ func TestGRPCQueryOrdersWithFilter(t *testing.T) { func TestGRPCQueryBidsWithFilter(t *testing.T) { suite := setupTest(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) + // creating bids with different states bidA, _ := createBid(t, suite.TestSuite) bidB, _ := createBid(t, suite.TestSuite) @@ -639,6 +654,20 @@ func TestGRPCQueryBidsWithFilter(t *testing.T) { func TestGRPCQueryLeasesWithFilter(t *testing.T) { suite := setupTest(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) + // creating leases with different states leaseA := createLease(t, suite.TestSuite) leaseB := createLease(t, suite.TestSuite) @@ -862,6 +891,20 @@ func TestGRPCQueryLeasesWithFilter(t *testing.T) { func TestGRPCQueryBid(t *testing.T) { suite := setupTest(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) + // creating bid bid, _ := createBid(t, suite.TestSuite) @@ -934,6 +977,19 @@ func TestGRPCQueryBid(t *testing.T) { func TestGRPCQueryBids(t *testing.T) { suite := setupTest(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) // creating bids with different states _, _ = createBid(t, suite.TestSuite) @@ -998,6 +1054,19 @@ func TestGRPCQueryBids(t *testing.T) { func TestGRPCQueryLease(t *testing.T) { suite := setupTest(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) // creating lease leaseID := createLease(t, suite.TestSuite) @@ -1073,6 +1142,19 @@ func TestGRPCQueryLease(t *testing.T) { func TestGRPCQueryLeases(t *testing.T) { suite := setupTest(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) // creating leases with different states leaseID := createLease(t, suite.TestSuite) diff --git a/x/market/keeper/keeper.go b/x/market/keeper/keeper.go index 107f983046..413f713394 100644 --- a/x/market/keeper/keeper.go +++ b/x/market/keeper/keeper.go @@ -357,6 +357,9 @@ func (k Keeper) OnGroupClosed(ctx sdk.Context, id dtypes.GroupID) error { if lease, ok := k.GetLease(ctx, bid.ID.LeaseID()); ok { // OnGroupClosed is callable by x/deployment only so only reason is owner err = k.OnLeaseClosed(ctx, lease, mv1.LeaseClosed, mv1.LeaseClosedReasonOwner) + if err != nil { + return err + } if err := k.ekeeper.PaymentClose(ctx, lease.ID.ToEscrowPaymentID()); err != nil { ctx.Logger().With("err", err).Info("error closing payment") } diff --git a/x/market/keeper/keeper_test.go b/x/market/keeper/keeper_test.go index 09ae00e538..b7aa87d68e 100644 --- a/x/market/keeper/keeper_test.go +++ b/x/market/keeper/keeper_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" sdk "github.com/cosmos/cosmos-sdk/types" @@ -367,5 +368,19 @@ func setupKeeper(t testing.TB) (sdk.Context, keeper.IKeeper, *state.TestSuite) { t.Helper() suite := state.SetupTestSuite(t) + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + + bkeeper. + On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + bkeeper. + On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }) + return suite.Context(), suite.MarketKeeper(), suite }