feat(hubl): cache bech32 prefix (#15954)
This commit is contained in:
parent
6a8251aafa
commit
1179285bf2
@ -1,6 +1,8 @@
|
||||
package autocli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
|
||||
"cosmossdk.io/client/v2/autocli/flag"
|
||||
"cosmossdk.io/core/address"
|
||||
@ -19,8 +21,8 @@ import (
|
||||
// var autoCliOpts autocli.AppOptions
|
||||
// err := depinject.Inject(appConfig, &encodingConfig.InterfaceRegistry, &autoCliOpts)
|
||||
//
|
||||
// If depinject isn't used, options can be provided manually or extracted from modules. One method for extracting autocli
|
||||
// options is via the github.com/cosmos/cosmos-sdk/runtime/services.ExtractAutoCLIOptions function.
|
||||
// If depinject isn't used, options can be provided manually or extracted from modules and the address codec can be provided by the auth keeper.
|
||||
// One method for extracting autocli options is via the github.com/cosmos/cosmos-sdk/runtime/services.ExtractAutoCLIOptions function.
|
||||
type AppOptions struct {
|
||||
depinject.In
|
||||
|
||||
@ -34,8 +36,7 @@ type AppOptions struct {
|
||||
ModuleOptions map[string]*autocliv1.ModuleOptions `optional:"true"`
|
||||
|
||||
// AddressCodec is the address codec to use for the app.
|
||||
// If not provided the default address prefix will be fetched from the reflection client.
|
||||
AddressCodec address.Codec `optional:"true"`
|
||||
AddressCodec address.Codec
|
||||
}
|
||||
|
||||
// EnhanceRootCommand enhances the provided root command with autocli AppOptions,
|
||||
@ -57,9 +58,6 @@ func (appOptions AppOptions) EnhanceRootCommand(rootCmd *cobra.Command) error {
|
||||
builder := &Builder{
|
||||
Builder: flag.Builder{
|
||||
AddressCodec: appOptions.AddressCodec,
|
||||
GetClientConn: func() (grpc.ClientConnInterface, error) {
|
||||
return client.GetClientQueryContext(rootCmd)
|
||||
},
|
||||
},
|
||||
GetClientConn: func(cmd *cobra.Command) (grpc.ClientConnInterface, error) {
|
||||
return client.GetClientQueryContext(cmd)
|
||||
@ -72,6 +70,10 @@ func (appOptions AppOptions) EnhanceRootCommand(rootCmd *cobra.Command) error {
|
||||
}
|
||||
|
||||
func (appOptions AppOptions) EnhanceRootCommandWithBuilder(rootCmd *cobra.Command, builder *Builder) error {
|
||||
if builder.AddressCodec == nil {
|
||||
return errors.New("address codec is required in builder")
|
||||
}
|
||||
|
||||
// extract any custom commands from modules
|
||||
customQueryCmds, customMsgCmds := map[string]*cobra.Command{}, map[string]*cobra.Command{}
|
||||
for name, module := range appOptions.Modules {
|
||||
|
||||
@ -6,15 +6,17 @@ import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
reflectionv2alpha1 "cosmossdk.io/api/cosmos/base/reflection/v2alpha1"
|
||||
"cosmossdk.io/client/v2/autocli/flag"
|
||||
"cosmossdk.io/client/v2/internal/testpb"
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
reflectionv2alpha1 "cosmossdk.io/api/cosmos/base/reflection/v2alpha1"
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
addresscodec "github.com/cosmos/cosmos-sdk/codec/address"
|
||||
|
||||
"cosmossdk.io/client/v2/autocli/flag"
|
||||
"cosmossdk.io/client/v2/internal/testpb"
|
||||
)
|
||||
|
||||
func testExecCommon(t *testing.T, buildModuleCommand func(string, *Builder) (*cobra.Command, error), args ...string) *testClientConn {
|
||||
@ -47,9 +49,7 @@ func testExecCommon(t *testing.T, buildModuleCommand func(string, *Builder) (*co
|
||||
}
|
||||
b := &Builder{
|
||||
Builder: flag.Builder{
|
||||
GetClientConn: func() (grpc.ClientConnInterface, error) {
|
||||
return conn, nil
|
||||
},
|
||||
AddressCodec: addresscodec.NewBech32Codec("cosmos"),
|
||||
},
|
||||
GetClientConn: func(*cobra.Command) (grpc.ClientConnInterface, error) {
|
||||
return conn, nil
|
||||
|
||||
@ -4,33 +4,14 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
reflectionv2alpha1 "cosmossdk.io/api/cosmos/base/reflection/v2alpha1"
|
||||
"cosmossdk.io/core/address"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
|
||||
addresscodec "github.com/cosmos/cosmos-sdk/codec/address"
|
||||
"cosmossdk.io/core/address"
|
||||
)
|
||||
|
||||
type addressStringType struct{}
|
||||
|
||||
func (a addressStringType) NewValue(ctx context.Context, b *Builder) Value {
|
||||
if b.AddressCodec == nil {
|
||||
conn, err := b.GetClientConn()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
reflectionClient := reflectionv2alpha1.NewReflectionServiceClient(conn)
|
||||
resp, err := reflectionClient.GetConfigurationDescriptor(ctx, &reflectionv2alpha1.GetConfigurationDescriptorRequest{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if resp == nil || resp.Config == nil {
|
||||
panic("bech32 account address prefix is not set")
|
||||
}
|
||||
|
||||
b.AddressCodec = addresscodec.NewBech32Codec(resp.Config.Bech32AccountAddressPrefix)
|
||||
}
|
||||
|
||||
return &addressValue{addressCodec: b.AddressCodec}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package flag
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/reflect/protodesc"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"google.golang.org/protobuf/reflect/protoregistry"
|
||||
@ -27,9 +26,6 @@ type Builder struct {
|
||||
|
||||
// AddressCodec is the address codec used for the address flag
|
||||
AddressCodec address.Codec
|
||||
|
||||
// GetClientConn is the reflection client for the address flag
|
||||
GetClientConn func() (grpc.ClientConnInterface, error)
|
||||
}
|
||||
|
||||
func (b *Builder) init() {
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
package address
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"cosmossdk.io/core/address"
|
||||
errorsmod "cosmossdk.io/errors"
|
||||
|
||||
@ -21,13 +24,17 @@ func NewBech32Codec(prefix string) address.Codec {
|
||||
|
||||
// StringToBytes encodes text to bytes
|
||||
func (bc Bech32Codec) StringToBytes(text string) ([]byte, error) {
|
||||
if len(strings.TrimSpace(text)) == 0 {
|
||||
return []byte{}, errors.New("empty address string is not allowed")
|
||||
}
|
||||
|
||||
hrp, bz, err := bech32.DecodeAndConvert(text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hrp != bc.Bech32Prefix {
|
||||
return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "hrp does not match bech32Prefix")
|
||||
return nil, errorsmod.Wrapf(sdkerrors.ErrLogic, "hrp does not match bech32 prefix: expected '%s' got '%s'", bc.Bech32Prefix, hrp)
|
||||
}
|
||||
|
||||
if err := sdk.VerifyAddressFormat(bz); err != nil {
|
||||
|
||||
@ -4,9 +4,10 @@ go 1.20
|
||||
|
||||
require (
|
||||
cosmossdk.io/api v0.4.1
|
||||
cosmossdk.io/client/v2 v2.0.0-20230320224637-dca0e7374a1d
|
||||
cosmossdk.io/client/v2 v2.0.0-20230426154441-2037a26d1235
|
||||
cosmossdk.io/errors v1.0.0-beta.7
|
||||
github.com/cockroachdb/errors v1.9.1
|
||||
github.com/cosmos/cosmos-sdk v0.46.0-beta2.0.20230426154441-2037a26d1235
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/pelletier/go-toml/v2 v2.0.7
|
||||
@ -17,7 +18,7 @@ require (
|
||||
|
||||
require (
|
||||
cosmossdk.io/collections v0.1.0 // indirect
|
||||
cosmossdk.io/core v0.6.1 // indirect
|
||||
cosmossdk.io/core v0.6.2-0.20230323161322-ccd8d40119e4 // indirect
|
||||
cosmossdk.io/depinject v1.0.0-alpha.3 // indirect
|
||||
cosmossdk.io/log v1.0.0 // indirect
|
||||
cosmossdk.io/math v1.0.0 // indirect
|
||||
@ -42,7 +43,6 @@ require (
|
||||
github.com/cosmos/btcutil v1.0.5 // indirect
|
||||
github.com/cosmos/cosmos-db v1.0.0-rc.1 // indirect
|
||||
github.com/cosmos/cosmos-proto v1.0.0-beta.3 // indirect
|
||||
github.com/cosmos/cosmos-sdk v0.46.0-beta2.0.20230424095137-b73c17cb9cc8 // indirect
|
||||
github.com/cosmos/go-bip39 v1.0.0 // indirect
|
||||
github.com/cosmos/gogoproto v1.4.8 // indirect
|
||||
github.com/cosmos/iavl v0.21.0-beta.1 // indirect
|
||||
|
||||
@ -37,12 +37,12 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
cosmossdk.io/api v0.4.1 h1:0ikaYM6GyxTYYcfBiyR8YnLCfhNnhKpEFnaSepCTmqg=
|
||||
cosmossdk.io/api v0.4.1/go.mod h1:jR7k5ok90LxW2lFUXvd8Vpo/dr4PpiyVegxdm7b1ZdE=
|
||||
cosmossdk.io/client/v2 v2.0.0-20230320224637-dca0e7374a1d h1:9mBIeO0ZhCalqS3pJiZ1fs+Nn93E7rU4+Hv7QVINbNM=
|
||||
cosmossdk.io/client/v2 v2.0.0-20230320224637-dca0e7374a1d/go.mod h1:qX4UABq4VI1ccJn4H4MIJx5/HvjRiaVaImovbnPXNXc=
|
||||
cosmossdk.io/client/v2 v2.0.0-20230426154441-2037a26d1235 h1:6aGhtjUgmacucrKMC9ZdF9G96YoxZqkTC2ZyxaAg1GE=
|
||||
cosmossdk.io/client/v2 v2.0.0-20230426154441-2037a26d1235/go.mod h1:ydI6QS3A+K2px6O8QpM0JtNaVV6lLeCJ5LVwtQXIMAg=
|
||||
cosmossdk.io/collections v0.1.0 h1:nzJGeiq32KnZroSrhB6rPifw4I85Cgmzw/YAmr4luv8=
|
||||
cosmossdk.io/collections v0.1.0/go.mod h1:xbauc0YsbUF8qKMVeBZl0pFCunxBIhKN/WlxpZ3lBuo=
|
||||
cosmossdk.io/core v0.6.1 h1:OBy7TI2W+/gyn2z40vVvruK3di+cAluinA6cybFbE7s=
|
||||
cosmossdk.io/core v0.6.1/go.mod h1:g3MMBCBXtxbDWBURDVnJE7XML4BG5qENhs0gzkcpuFA=
|
||||
cosmossdk.io/core v0.6.2-0.20230323161322-ccd8d40119e4 h1:l1scDTT2VX18ZuR6P0irvT/bAP0h4297D/Lka5nz2vE=
|
||||
cosmossdk.io/core v0.6.2-0.20230323161322-ccd8d40119e4/go.mod h1:J8R0E7soOpQFVqFiFd7EKepXCPpINa2n2t2EqbEsXnY=
|
||||
cosmossdk.io/depinject v1.0.0-alpha.3 h1:6evFIgj//Y3w09bqOUOzEpFj5tsxBqdc5CfkO7z+zfw=
|
||||
cosmossdk.io/depinject v1.0.0-alpha.3/go.mod h1:eRbcdQ7MRpIPEM5YUJh8k97nxHpYbc3sMUnEtt8HPWU=
|
||||
cosmossdk.io/errors v1.0.0-beta.7 h1:gypHW76pTQGVnHKo6QBkb4yFOJjC+sUGRc5Al3Odj1w=
|
||||
@ -146,8 +146,8 @@ github.com/cosmos/cosmos-db v1.0.0-rc.1 h1:SjnT8B6WKMW9WEIX32qMhnEEKcI7ZP0+G1Sa9
|
||||
github.com/cosmos/cosmos-db v1.0.0-rc.1/go.mod h1:Dnmk3flSf5lkwCqvvjNpoxjpXzhxnCAFzKHlbaForso=
|
||||
github.com/cosmos/cosmos-proto v1.0.0-beta.3 h1:VitvZ1lPORTVxkmF2fAp3IiA61xVwArQYKXTdEcpW6o=
|
||||
github.com/cosmos/cosmos-proto v1.0.0-beta.3/go.mod h1:t8IASdLaAq+bbHbjq4p960BvcTqtwuAxid3b/2rOD6I=
|
||||
github.com/cosmos/cosmos-sdk v0.46.0-beta2.0.20230424095137-b73c17cb9cc8 h1:zIl1WnrW5ZP1VwhpbwVBZtCntkNKYNIkg4233/dZ3BU=
|
||||
github.com/cosmos/cosmos-sdk v0.46.0-beta2.0.20230424095137-b73c17cb9cc8/go.mod h1:JicgV9n3SAu5uuoyDvQ2gSHYLyFvyRrIUYB5T2Q4HRw=
|
||||
github.com/cosmos/cosmos-sdk v0.46.0-beta2.0.20230426154441-2037a26d1235 h1:EQM4Ewp62TpQ141W0IJL84qJ8zyvGeqA75r++81JKGc=
|
||||
github.com/cosmos/cosmos-sdk v0.46.0-beta2.0.20230426154441-2037a26d1235/go.mod h1:1pnJEQxrWXGGijISBNKkisAuMlohZROsazmj+JYkBc0=
|
||||
github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y=
|
||||
github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY=
|
||||
github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw=
|
||||
|
||||
@ -15,6 +15,7 @@ type Config struct {
|
||||
|
||||
type ChainConfig struct {
|
||||
GRPCEndpoints []GRPCEndpoint `toml:"trusted-grpc-endpoints"`
|
||||
Bech32Prefix string `toml:"bech32-prefix"`
|
||||
}
|
||||
|
||||
type GRPCEndpoint struct {
|
||||
|
||||
@ -7,8 +7,6 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
|
||||
reflectionv1 "cosmossdk.io/api/cosmos/reflection/v1"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"google.golang.org/grpc"
|
||||
@ -18,6 +16,10 @@ import (
|
||||
"google.golang.org/protobuf/reflect/protodesc"
|
||||
"google.golang.org/protobuf/reflect/protoregistry"
|
||||
"google.golang.org/protobuf/types/descriptorpb"
|
||||
|
||||
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
|
||||
reflectionv2alpha1 "cosmossdk.io/api/cosmos/base/reflection/v2alpha1"
|
||||
reflectionv1 "cosmossdk.io/api/cosmos/reflection/v1"
|
||||
)
|
||||
|
||||
const DefaultConfigDirName = ".hubl"
|
||||
@ -25,20 +27,21 @@ const DefaultConfigDirName = ".hubl"
|
||||
type ChainInfo struct {
|
||||
client *grpc.ClientConn
|
||||
|
||||
ConfigDir string
|
||||
Chain string
|
||||
ModuleOptions map[string]*autocliv1.ModuleOptions
|
||||
Context context.Context
|
||||
ConfigDir string
|
||||
Chain string
|
||||
Config *ChainConfig
|
||||
|
||||
ProtoFiles *protoregistry.Files
|
||||
Context context.Context
|
||||
Config *ChainConfig
|
||||
ModuleOptions map[string]*autocliv1.ModuleOptions
|
||||
}
|
||||
|
||||
func NewChainInfo(configDir, chain string, config *ChainConfig) *ChainInfo {
|
||||
return &ChainInfo{
|
||||
ConfigDir: configDir,
|
||||
Chain: chain,
|
||||
Config: config,
|
||||
Context: context.Background(),
|
||||
Config: config,
|
||||
Chain: chain,
|
||||
ConfigDir: configDir,
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,22 +126,21 @@ func (c *ChainInfo) Load(reload bool) error {
|
||||
}
|
||||
|
||||
autocliQueryClient := autocliv1.NewQueryClient(client)
|
||||
appOptionsRes, err := autocliQueryClient.AppOptions(c.Context, &autocliv1.AppOptionsRequest{})
|
||||
appOptsRes, err := autocliQueryClient.AppOptions(c.Context, &autocliv1.AppOptionsRequest{})
|
||||
if err != nil {
|
||||
appOptionsRes = guessAutocli(c.ProtoFiles)
|
||||
appOptsRes = guessAutocli(c.ProtoFiles)
|
||||
}
|
||||
|
||||
bz, err := proto.Marshal(appOptionsRes)
|
||||
bz, err := proto.Marshal(appOptsRes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(appOptsFilename, bz, 0o600)
|
||||
if err != nil {
|
||||
if err := os.WriteFile(appOptsFilename, bz, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.ModuleOptions = appOptionsRes.ModuleOptions
|
||||
c.ModuleOptions = appOptsRes.ModuleOptions
|
||||
} else {
|
||||
bz, err := os.ReadFile(appOptsFilename)
|
||||
if err != nil {
|
||||
@ -146,8 +148,7 @@ func (c *ChainInfo) Load(reload bool) error {
|
||||
}
|
||||
|
||||
var appOptsRes autocliv1.AppOptionsResponse
|
||||
err = proto.Unmarshal(bz, &appOptsRes)
|
||||
if err != nil {
|
||||
if err := proto.Unmarshal(bz, &appOptsRes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -185,3 +186,18 @@ func (c *ChainInfo) OpenClient() (*grpc.ClientConn, error) {
|
||||
|
||||
return nil, errors.Wrapf(res, "error loading gRPC client")
|
||||
}
|
||||
|
||||
// getAddressPrefix returns the address prefix of the chain.
|
||||
func getAddressPrefix(ctx context.Context, conn grpc.ClientConnInterface) (string, error) {
|
||||
reflectionClient := reflectionv2alpha1.NewReflectionServiceClient(conn)
|
||||
resp, err := reflectionClient.GetConfigurationDescriptor(ctx, &reflectionv2alpha1.GetConfigurationDescriptorRequest{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp == nil || resp.Config == nil || resp.Config.Bech32AccountAddressPrefix == "" {
|
||||
return "", errors.New("bech32 account address prefix is not set")
|
||||
}
|
||||
|
||||
return resp.Config.Bech32AccountAddressPrefix, nil
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
@ -12,6 +13,8 @@ import (
|
||||
"google.golang.org/protobuf/reflect/protoregistry"
|
||||
"google.golang.org/protobuf/types/dynamicpb"
|
||||
|
||||
addresscodec "github.com/cosmos/cosmos-sdk/codec/address"
|
||||
|
||||
"cosmossdk.io/client/v2/autocli"
|
||||
"cosmossdk.io/client/v2/autocli/flag"
|
||||
)
|
||||
@ -91,22 +94,22 @@ func RemoteCommand(config *Config, configDir string) ([]*cobra.Command, error) {
|
||||
|
||||
builder := &autocli.Builder{
|
||||
Builder: flag.Builder{
|
||||
AddressCodec: addresscodec.NewBech32Codec(chainConfig.Bech32Prefix),
|
||||
TypeResolver: &dynamicTypeResolver{chainInfo},
|
||||
FileResolver: chainInfo.ProtoFiles,
|
||||
GetClientConn: func() (grpc.ClientConnInterface, error) {
|
||||
return chainInfo.OpenClient()
|
||||
},
|
||||
},
|
||||
GetClientConn: func(command *cobra.Command) (grpc.ClientConnInterface, error) {
|
||||
return chainInfo.OpenClient()
|
||||
},
|
||||
AddQueryConnFlags: func(command *cobra.Command) {},
|
||||
}
|
||||
|
||||
var (
|
||||
update bool
|
||||
reconfig bool
|
||||
insecure bool
|
||||
)
|
||||
|
||||
chainCmd := &cobra.Command{
|
||||
Use: chain,
|
||||
Short: fmt.Sprintf("Commands for the %s chain", chain),
|
||||
@ -177,7 +180,19 @@ func reconfigure(cmd *cobra.Command, config *Config, configDir, chain string) er
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := chainInfo.OpenClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addressPrefix, err := getAddressPrefix(context.Background(), client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chainConfig.Bech32Prefix = addressPrefix
|
||||
config.Chains[chain] = chainConfig
|
||||
|
||||
if err := SaveConfig(configDir, config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ func (bc bech32Codec) StringToBytes(text string) ([]byte, error) {
|
||||
}
|
||||
|
||||
if hrp != bc.bech32Prefix {
|
||||
return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "hrp does not match bech32Prefix")
|
||||
return nil, errorsmod.Wrapf(sdkerrors.ErrLogic, "hrp does not match bech32 prefix: expected '%s' got '%s'", bc.bech32Prefix, hrp)
|
||||
}
|
||||
|
||||
if err := sdk.VerifyAddressFormat(bz); err != nil {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user