From c41a061d8aa485bfe16c77f701b4e20fbdb7dade Mon Sep 17 00:00:00 2001 From: Artur Troian Date: Fri, 21 Nov 2025 10:30:48 -0600 Subject: [PATCH] refactor(x/escrow): truncate dec part during payment withdraw fixes issue when fractional of uakt remain in the balance after account is closed. refactor escrow testsuite to ensure funds are tracked during account operations and match expected values Signed-off-by: Artur Troian --- .github/workflows/tests.yaml | 8 +- app/app.go | 1 + app/types/app.go | 5 + go.mod | 2 +- go.sum | 4 +- make/test-upgrade.mk | 5 +- meta.json | 5 + tests/upgrade/sdktypes.go | 56 ++ tests/upgrade/test-cases.json | 12 +- tests/upgrade/test-config.json | 3 +- tests/upgrade/types/types.go | 1 + tests/upgrade/upgrade_test.go | 151 ++-- tests/upgrade/workers_test.go | 126 +--- testutil/state/suite.go | 15 +- upgrades/software/v1.1.0/init.go | 11 + upgrades/software/v1.1.0/upgrade.go | 466 ++++++++++++ upgrades/upgrades.go | 2 +- x/deployment/handler/handler_test.go | 129 +++- x/deployment/keeper/grpc_query_test.go | 77 +- x/escrow/genesis.go | 5 +- x/escrow/keeper/grpc_query.go | 6 +- x/escrow/keeper/grpc_query_test.go | 18 + x/escrow/keeper/keeper.go | 299 +++++--- x/escrow/keeper/keeper_settle_test.go | 28 + x/escrow/keeper/keeper_test.go | 299 ++++++-- x/market/handler/handler_test.go | 970 ++++++++++++++++++++++++- x/market/hooks/hooks.go | 63 +- x/market/keeper/grpc_query_test.go | 82 +++ x/market/keeper/keeper.go | 3 + x/market/keeper/keeper_test.go | 15 + 30 files changed, 2458 insertions(+), 409 deletions(-) create mode 100644 tests/upgrade/sdktypes.go create mode 100644 upgrades/software/v1.1.0/init.go create mode 100644 upgrades/software/v1.1.0/upgrade.go 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 }