Errors occurred while processing the request. Perhaps some objects are missing
@@ -57,11 +55,11 @@
- {{ $trimmedPrefix := trimPrefix $prefix }}
- {{if $trimmedPrefix }}
+ {{ $parentPrefix := getParent .Prefix }}
+ {{if $parentPrefix }}
- ⮐..
+ ⮐..
|
|
|
diff --git a/tree/tree.go b/tree/tree.go
index 2ee9356..d99e24b 100644
--- a/tree/tree.go
+++ b/tree/tree.go
@@ -7,7 +7,6 @@ import (
"strings"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/data"
- "git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/layer"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/logs"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/utils"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
@@ -52,10 +51,10 @@ type (
var (
// ErrNodeNotFound is returned from ServiceClient in case of not found error.
- ErrNodeNotFound = layer.ErrNodeNotFound
+ ErrNodeNotFound = errors.New("not found")
// ErrNodeAccessDenied is returned from ServiceClient service in case of access denied error.
- ErrNodeAccessDenied = layer.ErrNodeAccessDenied
+ ErrNodeAccessDenied = errors.New("access denied")
)
const (
@@ -259,7 +258,7 @@ func (c *Tree) getSystemNode(ctx context.Context, bktInfo *data.BucketInfo, name
nodes = filterMultipartNodes(nodes)
if len(nodes) == 0 {
- return nil, layer.ErrNodeNotFound
+ return nil, ErrNodeNotFound
}
if len(nodes) != 1 {
c.reqLogger(ctx).Warn(logs.FoundSeveralSystemTreeNodes, zap.String("name", name), logs.TagField(logs.TagExternalStorageTree))
@@ -303,7 +302,7 @@ func getLatestVersionNode(nodes []NodeResponse) (NodeResponse, error) {
}
if targetIndexNode == -1 {
- return nil, fmt.Errorf("latest version: %w", layer.ErrNodeNotFound)
+ return nil, fmt.Errorf("latest version: %w", ErrNodeNotFound)
}
return nodes[targetIndexNode], nil
@@ -324,20 +323,23 @@ func pathFromName(objectName string) []string {
return strings.Split(objectName, separator)
}
-func (c *Tree) GetSubTreeByPrefix(ctx context.Context, bktInfo *data.BucketInfo, prefix string, latestOnly bool) ([]data.NodeInfo, string, error) {
+func (c *Tree) GetSubTreeByPrefix(ctx context.Context, bktInfo *data.BucketInfo, prefix string, latestOnly bool) ([]data.NodeInfo, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "tree.GetSubTreeByPrefix")
defer span.End()
- rootID, tailPrefix, err := c.determinePrefixNode(ctx, bktInfo, versionTree, prefix)
+ rootID, err := c.getPrefixNodeID(ctx, bktInfo, versionTree, strings.Split(prefix, separator))
if err != nil {
- return nil, "", err
+ if errors.Is(err, ErrNodeNotFound) {
+ return nil, nil
+ }
+ return nil, err
}
subTree, err := c.service.GetSubTree(ctx, bktInfo, versionTree, rootID, 2, false)
if err != nil {
if errors.Is(err, ErrNodeNotFound) {
- return nil, "", nil
+ return nil, nil
}
- return nil, "", err
+ return nil, err
}
nodesMap := make(map[string][]NodeResponse, len(subTree))
@@ -347,10 +349,6 @@ func (c *Tree) GetSubTreeByPrefix(ctx context.Context, bktInfo *data.BucketInfo,
}
fileName := GetFilename(node)
- if !strings.HasPrefix(fileName, tailPrefix) {
- continue
- }
-
nodes := nodesMap[fileName]
// Add all nodes if flag latestOnly is false.
@@ -374,7 +372,7 @@ func (c *Tree) GetSubTreeByPrefix(ctx context.Context, bktInfo *data.BucketInfo,
result = append(result, nodeResponseToNodeInfo(nodes)...)
}
- return result, strings.TrimSuffix(prefix, tailPrefix), nil
+ return result, nil
}
func nodeResponseToNodeInfo(nodes []NodeResponse) []data.NodeInfo {
@@ -386,22 +384,6 @@ func nodeResponseToNodeInfo(nodes []NodeResponse) []data.NodeInfo {
return nodesInfo
}
-func (c *Tree) determinePrefixNode(ctx context.Context, bktInfo *data.BucketInfo, treeID, prefix string) ([]uint64, string, error) {
- rootID := []uint64{0}
- path := strings.Split(prefix, separator)
- tailPrefix := path[len(path)-1]
-
- if len(path) > 1 {
- var err error
- rootID, err = c.getPrefixNodeID(ctx, bktInfo, treeID, path[:len(path)-1])
- if err != nil {
- return nil, "", err
- }
- }
-
- return rootID, tailPrefix, nil
-}
-
func (c *Tree) getPrefixNodeID(ctx context.Context, bktInfo *data.BucketInfo, treeID string, prefixPath []string) ([]uint64, error) {
p := &GetNodesParams{
CnrID: bktInfo.CID,
@@ -424,7 +406,7 @@ func (c *Tree) getPrefixNodeID(ctx context.Context, bktInfo *data.BucketInfo, tr
}
if len(intermediateNodes) == 0 {
- return nil, layer.ErrNodeNotFound
+ return nil, ErrNodeNotFound
}
return intermediateNodes, nil
From 96a22d98f206ce4910d69ce68da221802cb23c22 Mon Sep 17 00:00:00 2001
From: Nikita Zinkevich
Date: Fri, 25 Apr 2025 10:03:16 +0300
Subject: [PATCH 11/11] [#232] Use contract to get container info
Signed-off-by: Nikita Zinkevich
---
cmd/http-gw/app.go | 28 ++++++-
cmd/http-gw/settings.go | 8 ++
config/config.env | 3 +
config/config.yaml | 5 ++
docs/gate-configuration.md | 13 ++++
go.mod | 2 +-
internal/handler/container.go | 42 +++++++++++
internal/handler/frostfs_mock.go | 10 +++
internal/handler/handler.go | 47 +++---------
internal/handler/handler_test.go | 2 +-
internal/logs/logs.go | 6 +-
.../service/contracts/container/client.go | 73 +++++++++++++++++++
internal/service/contracts/util/util.go | 34 +++++++++
13 files changed, 229 insertions(+), 44 deletions(-)
create mode 100644 internal/handler/container.go
create mode 100644 internal/service/contracts/container/client.go
create mode 100644 internal/service/contracts/util/util.go
diff --git a/cmd/http-gw/app.go b/cmd/http-gw/app.go
index f603d3b..4a83caf 100644
--- a/cmd/http-gw/app.go
+++ b/cmd/http-gw/app.go
@@ -22,6 +22,8 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/handler/middleware"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/logs"
internalnet "git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/net"
+ containerClient "git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/service/contracts/container"
+ contractsUtil "git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/service/contracts/util"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/service/frostfs"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/templates"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/metrics"
@@ -39,6 +41,7 @@ import (
"github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/cli/input"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
+ "github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/panjf2000/ants/v2"
@@ -276,6 +279,14 @@ func (a *app) initContainers(ctx context.Context) {
a.corsCnrID = *corsCnrID
}
+func (a *app) initRPCClient(ctx context.Context) *rpcclient.Client {
+ rpcCli, err := rpcclient.New(ctx, a.config().GetString(cfgRPCEndpoint), rpcclient.Options{})
+ if err != nil {
+ a.log.Fatal(logs.InitRPCClientFailed, zap.Error(err), logs.TagField(logs.TagApp))
+ }
+ return rpcCli
+}
+
func (a *app) initAppSettings(lc *logLevelConfig) {
a.settings = &appSettings{
reconnectInterval: fetchReconnectInterval(a.config()),
@@ -750,7 +761,22 @@ func (a *app) stopServices() {
}
func (a *app) configureRouter(workerPool *ants.Pool) {
- a.handle = handler.New(a.AppParams(), a.settings, tree.NewTree(frostfs.NewPoolWrapper(a.treePool), a.log), workerPool)
+ rpcCli := a.initRPCClient(a.ctx)
+ cnrContractName := a.config().GetString(cfgContractsContainerName)
+ rpcEndpoint := a.config().GetString(cfgRPCEndpoint)
+ cnrAddr, err := contractsUtil.ResolveContractHash(cnrContractName, rpcEndpoint)
+ if err != nil {
+ a.log.Fatal(logs.FailedToResolveContractHash, zap.Error(err), logs.TagField(logs.TagApp))
+ }
+ cnrClient, err := containerClient.New(containerClient.Config{
+ ContractHash: cnrAddr,
+ Key: a.key,
+ RPCClient: rpcCli,
+ })
+ if err != nil {
+ a.log.Fatal(logs.InitContainerContractFailed, zap.Error(err), logs.TagField(logs.TagApp))
+ }
+ a.handle = handler.New(a.AppParams(), a.settings, tree.NewTree(frostfs.NewPoolWrapper(a.treePool), a.log), cnrClient, workerPool)
r := router.New()
r.RedirectTrailingSlash = true
diff --git a/cmd/http-gw/settings.go b/cmd/http-gw/settings.go
index 07722de..4071969 100644
--- a/cmd/http-gw/settings.go
+++ b/cmd/http-gw/settings.go
@@ -62,6 +62,8 @@ const (
defaultMultinetFallbackDelay = 300 * time.Millisecond
+ defaultContainerContractName = "container.frostfs"
+
cfgServer = "server"
cfgTLSEnabled = "tls.enabled"
cfgTLSCertFile = "tls.cert_file"
@@ -197,6 +199,9 @@ const (
cmdConfig = "config"
cmdConfigDir = "config-dir"
cmdListenAddress = "listen_address"
+
+ // Contracts.
+ cfgContractsContainerName = "contracts.container.name"
)
var ignore = map[string]struct{}{
@@ -401,6 +406,9 @@ func setDefaults(v *viper.Viper, flags *pflag.FlagSet) {
// multinet
v.SetDefault(cfgMultinetFallbackDelay, defaultMultinetFallbackDelay)
+ // contracts
+ v.SetDefault(cfgContractsContainerName, defaultContainerContractName)
+
if resolveMethods, err := flags.GetStringSlice(cfgResolveOrder); err == nil {
v.SetDefault(cfgResolveOrder, resolveMethods)
}
diff --git a/config/config.env b/config/config.env
index a86f3e8..ff880d5 100644
--- a/config/config.env
+++ b/config/config.env
@@ -181,3 +181,6 @@ HTTP_GW_FEATURES_TREE_POOL_NETMAP_SUPPORT=true
# Containers properties
HTTP_GW_CONTAINERS_CORS=AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
+
+# Container contract hash (LE) or name in NNS.
+HTTP_GW_CONTRACTS_CONTAINER_NAME=container.frostfs
diff --git a/config/config.yaml b/config/config.yaml
index bb01d47..9b4b3c9 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -199,3 +199,8 @@ features:
containers:
cors: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
+
+contracts:
+ container:
+ # Container contract hash (LE) or name in NNS.
+ name: container.frostfs
diff --git a/docs/gate-configuration.md b/docs/gate-configuration.md
index 08e2679..7f3c4ef 100644
--- a/docs/gate-configuration.md
+++ b/docs/gate-configuration.md
@@ -60,6 +60,7 @@ $ cat http.log
| `multinet` | [Multinet configuration](#multinet-section) |
| `features` | [Features configuration](#features-section) |
| `containers` | [Containers configuration](#containers-section) |
+| `contracts` | [Contracts configuration](#contracts-section) |
# General section
@@ -527,3 +528,15 @@ containers:
| Parameter | Type | SIGHUP reload | Default value | Description |
|-----------|----------|---------------|---------------|-----------------------------------------|
| `cors` | `string` | no | | Container name for CORS configurations. |
+
+# `contracts` section
+
+```yaml
+contracts:
+ container:
+ name: container.frostfs
+```
+
+| Parameter | Type | SIGHUP reload | Default value | Description |
+|------------------|----------|---------------|---------------------|----------------------------------------------|
+| `container.name` | `string` | no | `container.frostfs` | Container contract hash (LE) or name in NNS. |
diff --git a/go.mod b/go.mod
index c065b57..6082ef6 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module git.frostfs.info/TrueCloudLab/frostfs-http-gw
go 1.23
require (
+ git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240621131249-49e5270f673e
git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20241125133852-37bd75821121
git.frostfs.info/TrueCloudLab/frostfs-qos v0.0.0-20250128150313-cfbca7fa1dfe
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20250317082814-87bb55f992dc
@@ -33,7 +34,6 @@ require (
require (
dario.cat/mergo v1.0.0 // indirect
- git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240621131249-49e5270f673e // indirect
git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 // indirect
git.frostfs.info/TrueCloudLab/hrw v1.2.1 // indirect
git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 // indirect
diff --git a/internal/handler/container.go b/internal/handler/container.go
new file mode 100644
index 0000000..3c7bec8
--- /dev/null
+++ b/internal/handler/container.go
@@ -0,0 +1,42 @@
+package handler
+
+import (
+ "context"
+ "fmt"
+
+ "git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/data"
+ "git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/logs"
+ "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
+ cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
+ "go.uber.org/zap"
+)
+
+func (h *Handler) containerInfo(ctx context.Context, cnrID cid.ID) (*data.BucketInfo, error) {
+ info := &data.BucketInfo{
+ CID: cnrID,
+ Name: cnrID.EncodeToString(),
+ }
+ res, err := h.cnrContract.GetContainerByID(cnrID)
+ if err != nil {
+ return nil, fmt.Errorf("get frostfs container: %w", err)
+ }
+
+ cnr := *res
+
+ if domain := container.ReadDomain(cnr); domain.Name() != "" {
+ info.Name = domain.Name()
+ info.Zone = domain.Zone()
+ }
+ info.HomomorphicHashDisabled = container.IsHomomorphicHashingDisabled(cnr)
+ info.PlacementPolicy = cnr.PlacementPolicy()
+
+ if err = h.cache.Put(info); err != nil {
+ h.reqLogger(ctx).Warn(logs.CouldntPutBucketIntoCache,
+ zap.String("bucket name", info.Name),
+ zap.Stringer("cid", info.CID),
+ zap.Error(err),
+ logs.TagField(logs.TagDatapath))
+ }
+
+ return info, nil
+}
diff --git a/internal/handler/frostfs_mock.go b/internal/handler/frostfs_mock.go
index 7d72ad9..540697f 100644
--- a/internal/handler/frostfs_mock.go
+++ b/internal/handler/frostfs_mock.go
@@ -233,6 +233,16 @@ func (t *TestFrostFS) SearchObjects(_ context.Context, prm PrmObjectSearch) (Res
return &resObjectSearchMock{res: res}, nil
}
+func (t *TestFrostFS) GetContainerByID(cid cid.ID) (*container.Container, error) {
+ for k, v := range t.containers {
+ if k == cid.EncodeToString() {
+ return v, nil
+ }
+ }
+
+ return nil, fmt.Errorf("container does not exist %s", cid)
+}
+
func (t *TestFrostFS) InitMultiObjectReader(context.Context, PrmInitMultiObjectReader) (io.Reader, error) {
return nil, nil
}
diff --git a/internal/handler/handler.go b/internal/handler/handler.go
index 4d1dc31..2efd71d 100644
--- a/internal/handler/handler.go
+++ b/internal/handler/handler.go
@@ -167,12 +167,18 @@ type ContainerResolver interface {
Resolve(ctx context.Context, zone, name string) (*cid.ID, error)
}
+type ContainerContract interface {
+ // GetContainerByID reads a container from contract by ID.
+ GetContainerByID(cid.ID) (*container.Container, error)
+}
+
type Handler struct {
log *zap.Logger
frostfs FrostFS
ownerID *user.ID
config Config
containerResolver ContainerResolver
+ cnrContract ContainerContract
tree *tree.Tree
cache *cache.BucketCache
workerPool *ants.Pool
@@ -190,7 +196,7 @@ type AppParams struct {
CORSCache *cache.CORSCache
}
-func New(params *AppParams, config Config, tree *tree.Tree, workerPool *ants.Pool) *Handler {
+func New(params *AppParams, config Config, tree *tree.Tree, rpcCli ContainerContract, workerPool *ants.Pool) *Handler {
return &Handler{
log: params.Logger,
frostfs: params.FrostFS,
@@ -202,6 +208,7 @@ func New(params *AppParams, config Config, tree *tree.Tree, workerPool *ants.Poo
workerPool: workerPool,
corsCnrID: params.CORSCnrID,
corsCache: params.CORSCache,
+ cnrContract: rpcCli,
}
}
@@ -308,43 +315,7 @@ func (h *Handler) getBucketInfo(ctx context.Context, containerName string) (*dat
return nil, fmt.Errorf("resolve container: %w", err)
}
- bktInfo, err := h.readContainer(ctx, *cnrID)
- if err != nil {
- return nil, fmt.Errorf("read container: %w", err)
- }
-
- if err = h.cache.Put(bktInfo); err != nil {
- h.reqLogger(ctx).Warn(logs.CouldntPutBucketIntoCache,
- zap.String("bucket name", bktInfo.Name),
- zap.Stringer("bucket cid", bktInfo.CID),
- zap.Error(err),
- logs.TagField(logs.TagDatapath))
- }
-
- return bktInfo, nil
-}
-
-func (h *Handler) readContainer(ctx context.Context, cnrID cid.ID) (*data.BucketInfo, error) {
- prm := PrmContainer{ContainerID: cnrID}
- res, err := h.frostfs.Container(ctx, prm)
- if err != nil {
- return nil, fmt.Errorf("get frostfs container '%s': %w", cnrID.String(), err)
- }
-
- bktInfo := &data.BucketInfo{
- CID: cnrID,
- Name: cnrID.EncodeToString(),
- }
-
- if domain := container.ReadDomain(*res); domain.Name() != "" {
- bktInfo.Name = domain.Name()
- bktInfo.Zone = domain.Zone()
- }
-
- bktInfo.HomomorphicHashDisabled = container.IsHomomorphicHashingDisabled(*res)
- bktInfo.PlacementPolicy = res.PlacementPolicy()
-
- return bktInfo, err
+ return h.containerInfo(ctx, *cnrID)
}
type ListFunc func(ctx context.Context, bucketInfo *data.BucketInfo, prefix string) (*GetObjectsResponse, error)
diff --git a/internal/handler/handler_test.go b/internal/handler/handler_test.go
index 622940e..6c715fe 100644
--- a/internal/handler/handler_test.go
+++ b/internal/handler/handler_test.go
@@ -156,7 +156,7 @@ func prepareHandlerContextBase(logger *zap.Logger) (*handlerContext, error) {
if err != nil {
return nil, err
}
- handler := New(params, cfgMock, tree.NewTree(treeMock, logger), workerPool)
+ handler := New(params, cfgMock, tree.NewTree(treeMock, logger), testFrostFS, workerPool)
return &handlerContext{
key: key,
diff --git a/internal/logs/logs.go b/internal/logs/logs.go
index e7d118f..86921dd 100644
--- a/internal/logs/logs.go
+++ b/internal/logs/logs.go
@@ -73,6 +73,9 @@ const (
FailedToReadIndexPageTemplate = "failed to read index page template"
SetCustomIndexPageTemplate = "set custom index page template"
CouldNotFetchCORSContainerInfo = "couldn't fetch CORS container info"
+ InitRPCClientFailed = "init rpc client faileds"
+ InitContainerContractFailed = "init container contract failed"
+ FailedToResolveContractHash = "failed to resolve contract hash"
)
// Log messages with the "datapath" tag.
@@ -107,9 +110,7 @@ const (
IteratingOverSelectedObjectsFailed = "iterating over selected objects failed"
FailedToGetBucketInfo = "could not get bucket info"
FailedToSubmitTaskToPool = "failed to submit task to pool"
- ObjectWasDeleted = "object was deleted"
IndexWasDeleted = "index was deleted"
- FailedToGetLatestVersionOfObject = "failed to get latest version of object"
FailedToGetLatestVersionOfIndexObject = "failed to get latest version of index object"
FailedToCheckIfSettingsNodeExist = "failed to check if settings node exists"
FailedToListObjects = "failed to list objects"
@@ -121,7 +122,6 @@ const (
FailedToGetObjectPayload = "failed to get object payload"
FailedToFindObjectByAttribute = "failed to get find object by attribute"
FailedToUnescapePath = "failed to unescape path"
- InvalidOIDParam = "invalid oid param"
CouldNotGetCORSConfiguration = "could not get cors configuration"
EmptyOriginRequestHeader = "empty Origin request header"
EmptyAccessControlRequestMethodHeader = "empty Access-Control-Request-Method request header"
diff --git a/internal/service/contracts/container/client.go b/internal/service/contracts/container/client.go
new file mode 100644
index 0000000..09455be
--- /dev/null
+++ b/internal/service/contracts/container/client.go
@@ -0,0 +1,73 @@
+package container
+
+import (
+ "fmt"
+ "strings"
+
+ containercontract "git.frostfs.info/TrueCloudLab/frostfs-contract/container"
+ containerclient "git.frostfs.info/TrueCloudLab/frostfs-contract/rpcclient/container"
+ "git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/handler"
+ "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
+ cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
+ "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
+ "github.com/nspcc-dev/neo-go/pkg/rpcclient"
+ "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
+ "github.com/nspcc-dev/neo-go/pkg/util"
+ "github.com/nspcc-dev/neo-go/pkg/wallet"
+)
+
+type Client struct {
+ contract *containerclient.Contract
+}
+
+type Config struct {
+ ContractHash util.Uint160
+ Key *keys.PrivateKey
+ RPCClient *rpcclient.Client
+}
+
+func New(cfg Config) (*Client, error) {
+ var err error
+ key := cfg.Key
+ if key == nil {
+ if key, err = keys.NewPrivateKey(); err != nil {
+ return nil, fmt.Errorf("generate anon private key for container contract: %w", err)
+ }
+ }
+ acc := wallet.NewAccountFromPrivateKey(key)
+
+ act, err := actor.NewSimple(cfg.RPCClient, acc)
+ if err != nil {
+ return nil, fmt.Errorf("create new actor: %w", err)
+ }
+
+ return &Client{
+ contract: containerclient.New(act, cfg.ContractHash),
+ }, nil
+}
+
+func (c *Client) GetContainerByID(cnrID cid.ID) (*container.Container, error) {
+ items, err := c.contract.Get(cnrID[:])
+ if err != nil {
+ if strings.Contains(err.Error(), containercontract.NotFoundError) {
+ return nil, fmt.Errorf("%w: %s", handler.ErrContainerNotFound, err)
+ }
+ return nil, err
+ }
+
+ if len(items) != 4 {
+ return nil, fmt.Errorf("unexpected container stack item count: %d", len(items))
+ }
+
+ cnrBytes, err := items[0].TryBytes()
+ if err != nil {
+ return nil, fmt.Errorf("could not get byte array of container: %w", err)
+ }
+
+ var cnr container.Container
+ if err = cnr.Unmarshal(cnrBytes); err != nil {
+ return nil, fmt.Errorf("can't unmarshal container: %w", err)
+ }
+
+ return &cnr, nil
+}
diff --git a/internal/service/contracts/util/util.go b/internal/service/contracts/util/util.go
new file mode 100644
index 0000000..444504b
--- /dev/null
+++ b/internal/service/contracts/util/util.go
@@ -0,0 +1,34 @@
+package util
+
+import (
+ "fmt"
+ "strings"
+
+ "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
+ "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ns"
+ "github.com/nspcc-dev/neo-go/pkg/util"
+)
+
+// ResolveContractHash determine contract hash by resolving NNS name.
+func ResolveContractHash(contractHash, rpcAddress string) (util.Uint160, error) {
+ if hash, err := util.Uint160DecodeStringLE(contractHash); err == nil {
+ return hash, nil
+ }
+
+ splitName := strings.Split(contractHash, ".")
+ if len(splitName) != 2 {
+ return util.Uint160{}, fmt.Errorf("invalid contract name: '%s'", contractHash)
+ }
+
+ var domain container.Domain
+ domain.SetName(splitName[0])
+ domain.SetZone(splitName[1])
+
+ var nns ns.NNS
+ if err := nns.Dial(rpcAddress); err != nil {
+ return util.Uint160{}, fmt.Errorf("dial nns %s: %w", rpcAddress, err)
+ }
+ defer nns.Close()
+
+ return nns.ResolveContractHash(domain)
+}