Skip to content

Commit

Permalink
[Session] Add support for GetSession via the CLI + deps (#123)
Browse files Browse the repository at this point in the history
    - Update the CLI code flow to be able to call `GetSession`
    - Add make targets to trigger `GetSession`
    - Replace `[]` with `<>` in all CLI functions to differentiate required vs optional params
    - Made a couple changes to `SessionHydrator` (and added tests) to account additional error cases

    https://github.com/pokt-network/poktroll/assets/1892194/56425906-d06f-4c41-b8a5-d210d8a450cd

    ---

    Co-authored-by: Bryan White <[email protected]>
    Co-authored-by: Daniel Olshansky <[email protected]>
    Co-authored-by: Dima Kniazev <[email protected]>
  • Loading branch information
Olshansk committed Nov 8, 2023
1 parent b4f7ec6 commit 0a005b0
Show file tree
Hide file tree
Showing 30 changed files with 709 additions and 146 deletions.
59 changes: 51 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -312,31 +312,37 @@ app_delegate: ## Delegate trust to a gateway (must specify the APP and GATEWAY_A

.PHONY: app1_delegate_gateway1
app1_delegate_gateway1: ## Delegate trust to gateway1
APP=app1 GATEWAY_ADDR=pokt15vzxjqklzjtlz7lahe8z2dfe9nm5vxwwmscne4 make app_delegate
GATEWAY1=$$(make poktrolld_addr ACC_NAME=gateway1) && \
APP=app1 GATEWAY_ADDR=$$GATEWAY1 make app_delegate

.PHONY: app2_delegate_gateway2
app2_delegate_gateway2: ## Delegate trust to gateway2
APP=app2 GATEWAY_ADDR=pokt15w3fhfyc0lttv7r585e2ncpf6t2kl9uh8rsnyz make app_delegate
GATEWAY2=$$(make poktrolld_addr ACC_NAME=gateway2) && \
APP=app2 GATEWAY_ADDR=$$GATEWAY2 make app_delegate

.PHONY: app3_delegate_gateway3
app3_delegate_gateway3: ## Delegate trust to gateway3
APP=app3 GATEWAY_ADDR=pokt1zhmkkd0rh788mc9prfq0m2h88t9ge0j83gnxya make app_delegate
GATEWAY3=$$(make poktrolld_addr ACC_NAME=gateway3) && \
APP=app3 GATEWAY_ADDR=$$GATEWAY3 make app_delegate

.PHONY: app_undelegate
app_undelegate: ## Undelegate trust to a gateway (must specify the APP and GATEWAY_ADDR env vars). Requires the app to be staked
poktrolld --home=$(POKTROLLD_HOME) tx application undelegate-from-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE)

.PHONY: app1_undelegate_gateway1
app1_undelegate_gateway1: ## Undelegate trust to gateway1
APP=app1 GATEWAY_ADDR=pokt15vzxjqklzjtlz7lahe8z2dfe9nm5vxwwmscne4 make app_undelegate
GATEWAY1=$$(make poktrolld_addr ACC_NAME=gateway1) && \
APP=app1 GATEWAY_ADDR=$$GATEWAY1 make app_undelegate

.PHONY: app2_undelegate_gateway2
app2_undelegate_gateway2: ## Undelegate trust to gateway2
APP=app2 GATEWAY_ADDR=pokt15w3fhfyc0lttv7r585e2ncpf6t2kl9uh8rsnyz make app_undelegate
GATEWAY2=$$(make poktrolld_addr ACC_NAME=gateway2) && \
APP=app2 GATEWAY_ADDR=$$GATEWAY2 make app_undelegate

.PHONY: app3_undelegate_gateway3
app3_undelegate_gateway3: ## Undelegate trust to gateway3
APP=app3 GATEWAY_ADDR=pokt1zhmkkd0rh788mc9prfq0m2h88t9ge0j83gnxya make app_undelegate
GATEWAY3=$$(make poktrolld_addr ACC_NAME=gateway3) && \
APP=app3 GATEWAY_ADDR=$$GATEWAY3 make app_undelegate

#################
### Suppliers ###
Expand Down Expand Up @@ -380,6 +386,29 @@ supplier2_unstake: ## Unstake supplier2
supplier3_unstake: ## Unstake supplier3
SUPPLIER=supplier3 make supplier_unstake

###############
### Session ###
###############

.PHONY: get_session
get_session: ## Retrieve the session given the following env vars: (APP_ADDR, SVC, HEIGHT)
pocketd --home=$(POCKETD_HOME) q session get-session $(APP) $(SVC) $(HEIGHT) --node $(POCKET_NODE)

.PHONY: get_session_app1_anvil
get_session_app1_anvil: ## Retrieve the session for (app1, anvil, latest_height)
APP1=$$(make poktrolld_addr ACC_NAME=app1) && \
APP=$$APP1 SVC=anvil HEIGHT=0 make get_session

.PHONY: get_session_app2_anvil
get_session_app2_anvil: ## Retrieve the session for (app2, anvil, latest_height)
APP2=$$(make poktrolld_addr ACC_NAME=app2) && \
APP=$$APP2 SVC=anvil HEIGHT=0 make get_session

.PHONY: get_session_app3_anvil
get_session_app3_anvil: ## Retrieve the session for (app3, anvil, latest_height)
APP3=$$(make poktrolld_addr ACC_NAME=app3) && \
APP=$$APP3 SVC=anvil HEIGHT=0 make get_session

################
### Accounts ###
################
Expand All @@ -398,11 +427,13 @@ acc_balance_query_module_app: ## Query the balance of the network level "applica

.PHONY: acc_balance_query_module_supplier
acc_balance_query_module_supplier: ## Query the balance of the network level "supplier" module
make acc_balance_query ACC=pokt1j40dzzmn6cn9kxku7a5tjnud6hv37vesr5ccaa
SUPPLIER1=$(make poktrolld_addr ACC_NAME=supplier1)
make acc_balance_query ACC=SUPPLIER1

.PHONY: acc_balance_query_app1
acc_balance_query_app1: ## Query the balance of app1
make acc_balance_query ACC=pokt1mrqt5f7qh8uxs27cjm9t7v9e74a9vvdnq5jva4
APP1=$$(make poktrolld_addr ACC_NAME=app1) && \
make acc_balance_query ACC=$$APP1

.PHONY: acc_balance_total_supply
acc_balance_total_supply: ## Query the total supply of the network
Expand All @@ -428,8 +459,20 @@ trigger_ci: ## Trigger the CI pipeline by submitting an empty commit; See https:
#####################
### Documentation ###
#####################

.PHONY: go_docs
go_docs: check_godoc ## Generate documentation for the project
echo "Visit http://localhost:6060/pkg/pocket/"
godoc -http=:6060

.PHONY: openapi_gen
openapi_gen: ## Generate the OpenAPI spec for the Ignite API
ignite generate openapi --yes

######################
### Ignite Helpers ###
######################

.PHONY: poktrolld_addr
poktrolld_addr: ## Retrieve the address for an account by ACC_NAME
@echo $(shell poktrolld keys show -a $(ACC_NAME))
22 changes: 11 additions & 11 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -575,17 +575,6 @@ func New(
)
serviceModule := servicemodule.NewAppModule(appCodec, app.ServiceKeeper, app.AccountKeeper, app.BankKeeper)

app.SessionKeeper = *sessionmodulekeeper.NewKeeper(
appCodec,
keys[sessionmoduletypes.StoreKey],
keys[sessionmoduletypes.MemStoreKey],
app.GetSubspace(sessionmoduletypes.ModuleName),

app.ApplicationKeeper,
app.SupplierKeeper,
)
sessionModule := sessionmodule.NewAppModule(appCodec, app.SessionKeeper, app.AccountKeeper, app.BankKeeper)

app.SupplierKeeper = *suppliermodulekeeper.NewKeeper(
appCodec,
keys[suppliermoduletypes.StoreKey],
Expand Down Expand Up @@ -618,6 +607,17 @@ func New(
)
applicationModule := applicationmodule.NewAppModule(appCodec, app.ApplicationKeeper, app.AccountKeeper, app.BankKeeper)

app.SessionKeeper = *sessionmodulekeeper.NewKeeper(
appCodec,
keys[sessionmoduletypes.StoreKey],
keys[sessionmoduletypes.MemStoreKey],
app.GetSubspace(sessionmoduletypes.ModuleName),

app.ApplicationKeeper,
app.SupplierKeeper,
)
sessionModule := sessionmodule.NewAppModule(appCodec, app.SessionKeeper, app.AccountKeeper, app.BankKeeper)

// this line is used by starport scaffolding # stargate/app/keeperDefinition

/**** IBC Routing ****/
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/cosmos/cosmos-sdk v0.47.3
github.com/cosmos/gogoproto v1.4.10
github.com/cosmos/ibc-go/v7 v7.1.0
github.com/gogo/status v1.1.1
github.com/golang/mock v1.6.0
github.com/golang/protobuf v1.5.3
github.com/gorilla/mux v1.8.0
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -688,10 +688,13 @@ github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI=
github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/googleapis v1.4.1-0.20201022092350-68b0159b7869/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0=
github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
github.com/gogo/status v1.1.1 h1:DuHXlSFHNKqTQ+/ACf5Vs6r4X/dH2EgIzR9Vr+H65kg=
github.com/gogo/status v1.1.1/go.mod h1:jpG3dM5QPcqu19Hg8lkUhBFBa3TcLs1DG7+2Jqci7oU=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8=
Expand Down Expand Up @@ -2610,6 +2613,7 @@ google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
Expand Down Expand Up @@ -2735,6 +2739,7 @@ google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnp
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
Expand Down
4 changes: 2 additions & 2 deletions proto/pocket/shared/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ message Service {

// ApplicationServiceConfig holds the service configuration the application stakes for
message ApplicationServiceConfig {
Service service = 1; // The Service for which the application is configured for
Service service = 1; // The Service for which the application is configured

// TODO_RESEARCH: There is an opportunity for applications to advertise the max
// they're willing to pay for a certain configuration/price, but this is outside of scope.
Expand All @@ -28,7 +28,7 @@ message ApplicationServiceConfig {

// SupplierServiceConfig holds the service configuration the supplier stakes for
message SupplierServiceConfig {
Service service = 1; // The Service for which the supplier is configured for
Service service = 1; // The Service for which the supplier is configured
repeated SupplierEndpoint endpoints = 2; // List of endpoints for the service
// TODO_RESEARCH: There is an opportunity for supplier to advertise the min
// they're willing to earn for a certain configuration/price, but this is outside of scope.
Expand Down
35 changes: 28 additions & 7 deletions testutil/keeper/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,15 @@ import (
type option[V any] func(k *keeper.Keeper)

var (
TestServiceId1 = "svc1"
TestServiceId2 = "svc2"
TestServiceId1 = "svc1" // staked for by app1 & supplier1
TestServiceId11 = "svc11" // staked for by app1

TestApp1Address = "pokt106grzmkmep67pdfrm6ccl9snynryjqus6l3vct" // Generated via sample.AccAddress()
TestServiceId2 = "svc2" // staked for by app2 & supplier1
TestServiceId22 = "svc22" // staked for by app2

TestServiceId12 = "svc12" // staked for by app1, app2 & supplier1

TestApp1Address = "pokt1mdccn4u38eyjdxkk4h0jaddw4n3c72u82m5m9e" // Generated via sample.AccAddress()
TestApp1 = apptypes.Application{
Address: TestApp1Address,
Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)},
Expand All @@ -39,21 +44,27 @@ var (
Service: &sharedtypes.Service{Id: TestServiceId1},
},
{
Service: &sharedtypes.Service{Id: TestServiceId2},
Service: &sharedtypes.Service{Id: TestServiceId11},
},
{
Service: &sharedtypes.Service{Id: TestServiceId12},
},
},
}

TestApp2Address = "pokt1dm7tr0a99ja232gzt5rjtrl7hj6z6h40669fwh" // Generated via sample.AccAddress()
TestApp2Address = "pokt133amv5suh75zwkxxcq896azvmmwszg99grvk9f" // Generated via sample.AccAddress()
TestApp2 = apptypes.Application{
Address: TestApp1Address,
Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)},
ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{
{
Service: &sharedtypes.Service{Id: TestServiceId1},
Service: &sharedtypes.Service{Id: TestServiceId2},
},
{
Service: &sharedtypes.Service{Id: TestServiceId2},
Service: &sharedtypes.Service{Id: TestServiceId22},
},
{
Service: &sharedtypes.Service{Id: TestServiceId12},
},
},
}
Expand Down Expand Up @@ -84,6 +95,16 @@ var (
},
},
},
{
Service: &sharedtypes.Service{Id: TestServiceId12},
Endpoints: []*sharedtypes.SupplierEndpoint{
{
Url: TestSupplierUrl,
RpcType: sharedtypes.RPCType_GRPC,
Configs: make([]*sharedtypes.ConfigOption, 0),
},
},
},
},
}
)
Expand Down
3 changes: 3 additions & 0 deletions testutil/network/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ func DefaultApplicationModuleGenesisState(t *testing.T, n int) *apptypes.Genesis
{
Service: &sharedtypes.Service{Id: fmt.Sprintf("svc%d", i)},
},
{
Service: &sharedtypes.Service{Id: fmt.Sprintf("svc%d%d", i, i)},
},
},
}
// TODO_CONSIDERATION: Evaluate whether we need `nullify.Fill` or if we should enforce `(gogoproto.nullable) = false` everywhere
Expand Down
2 changes: 1 addition & 1 deletion x/application/client/cli/query_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func CmdListApplication() *cobra.Command {

func CmdShowApplication() *cobra.Command {
cmd := &cobra.Command{
Use: "show-application [address]",
Use: "show-application <application_address>",
Short: "shows a application",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) (err error) {
Expand Down
2 changes: 1 addition & 1 deletion x/application/client/cli/tx_delegate_to_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var _ = strconv.Itoa(0)

func CmdDelegateToGateway() *cobra.Command {
cmd := &cobra.Command{
Use: "delegate-to-gateway [gateway address]",
Use: "delegate-to-gateway <gateway_address>",
Short: "Delegate an application to a gateway",
Long: `Delegate an application to the gateway with the provided address. This is a broadcast operation
that delegates authority to the gateway specified to sign relays requests for the application, allowing the gateway
Expand Down
2 changes: 1 addition & 1 deletion x/application/client/cli/tx_stake_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func CmdStakeApplication() *cobra.Command {
// TODO_HACK: For now we are only specifying the service IDs as a list of of strings separated by commas.
// This needs to be expand to specify the full ApplicationServiceConfig. Furthermore, providing a flag to
// a file where ApplicationServiceConfig specifying full service configurations in the CLI by providing a flag that accepts a JSON string
Use: "stake-application [amount] [svcId1,svcId2,...,svcIdN]",
Use: "stake-application <upokt_amount> <svcId1,svcId2,...,svcIdN>",
Short: "Stake an application",
Long: `Stake an application with the provided parameters. This is a broadcast operation that
will stake the tokens and serviceIds and associate them with the application specified by the 'from' address.
Expand Down
2 changes: 1 addition & 1 deletion x/application/client/cli/tx_unstake_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ var _ = strconv.Itoa(0)
func CmdUnstakeApplication() *cobra.Command {
// fromAddress & signature is retrieved via `flags.FlagFrom` in the `clientCtx`
cmd := &cobra.Command{
Use: "unstake-application [amount]",
Use: "unstake-application",
Short: "Unstake an application",
Long: `Unstake an application. This is a broadcast operation that will unstake
the application specified by the 'from' address.
Expand Down
14 changes: 12 additions & 2 deletions x/gateway/client/cli/helpers_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
package cli_test

import (
"strconv"
"testing"

"github.com/stretchr/testify/require"

"github.com/pokt-network/poktroll/cmd/pocketd/cmd"
"github.com/pokt-network/poktroll/testutil/network"
"github.com/pokt-network/poktroll/x/gateway/types"

"github.com/stretchr/testify/require"
)

// Dummy variable to avoid unused import error.
var _ = strconv.IntSize

// init initializes the SDK configuration.
func init() {
cmd.InitSDKConfig()
}

// networkWithGatewayObjects creates a network with a populated gateway state of n gateway objects
func networkWithGatewayObjects(t *testing.T, n int) (*network.Network, []types.Gateway) {
t.Helper()
Expand Down
2 changes: 1 addition & 1 deletion x/gateway/client/cli/query_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func CmdListGateway() *cobra.Command {

func CmdShowGateway() *cobra.Command {
cmd := &cobra.Command{
Use: "show-gateway [address]",
Use: "show-gateway <gateway_address>",
Short: "shows a gateway",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) (err error) {
Expand Down
6 changes: 3 additions & 3 deletions x/gateway/client/cli/tx_stake_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ package cli
import (
"strconv"

"github.com/pokt-network/poktroll/x/gateway/types"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/spf13/cobra"

"github.com/pokt-network/poktroll/x/gateway/types"
)

var _ = strconv.Itoa(0)

func CmdStakeGateway() *cobra.Command {
cmd := &cobra.Command{
Use: "stake-gateway [amount]",
Use: "stake-gateway <upokt_amount>",
Short: "Stake a gateway",
Long: `Stake a gateway with the provided parameters. This is a broadcast operation that
will stake the tokens and associate them with the gateway specified by the 'from' address.
Expand Down
2 changes: 1 addition & 1 deletion x/gateway/client/cli/tx_unstake_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ var _ = strconv.Itoa(0)
func CmdUnstakeGateway() *cobra.Command {
// fromAddress & signature is retrieved via `flags.FlagFrom` in the `clientCtx`
cmd := &cobra.Command{
Use: "unstake-gateway [amount]",
Use: "unstake-gateway <upokt_amount>",
Short: "Unstake a gateway",
Long: `Unstake a gateway. This is a broadcast operation that will unstake the gateway specified by the 'from' address.
Expand Down
Loading

0 comments on commit 0a005b0

Please sign in to comment.