169 lines
5 KiB
Go
169 lines
5 KiB
Go
package ns
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
|
|
"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/core/state"
|
|
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
|
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
)
|
|
|
|
// multiSchemeClient unites invoker.RPCInvoke and common interface of
|
|
// rpcclient.Client and rpcclient.WSClient.
|
|
type multiSchemeClient interface {
|
|
invoker.RPCInvoke
|
|
// Init turns client to "ready-to-work" state.
|
|
Init() error
|
|
// Close closes connections.
|
|
Close()
|
|
// GetContractStateByID returns state of the NNS contract on 1 input.
|
|
GetContractStateByID(int32) (*state.Contract, error)
|
|
}
|
|
|
|
// NNS looks up FrostFS names using Neo Name Service.
|
|
//
|
|
// Instances are created with a variable declaration. Before work, the connection
|
|
// to the NNS server MUST be established using Dial method.
|
|
type NNS struct {
|
|
nnsContract util.Uint160
|
|
client multiSchemeClient
|
|
|
|
invoker interface {
|
|
Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error)
|
|
}
|
|
}
|
|
|
|
// Dial connects to the address of the NNS server. If fails, the instance
|
|
// MUST NOT be used.
|
|
//
|
|
// If URL address scheme is 'ws' or 'wss', then WebSocket protocol is used,
|
|
// otherwise HTTP.
|
|
func (n *NNS) Dial(address string) error {
|
|
var err error
|
|
|
|
uri, err := url.Parse(address)
|
|
if err == nil && (uri.Scheme == "ws" || uri.Scheme == "wss") {
|
|
n.client, err = rpcclient.NewWS(context.Background(), address, rpcclient.WSOptions{})
|
|
if err != nil {
|
|
return fmt.Errorf("create Neo WebSocket client: %w", err)
|
|
}
|
|
} else {
|
|
n.client, err = rpcclient.New(context.Background(), address, rpcclient.Options{})
|
|
if err != nil {
|
|
return fmt.Errorf("create Neo HTTP client: %w", err)
|
|
}
|
|
}
|
|
|
|
if err = n.client.Init(); err != nil {
|
|
return fmt.Errorf("initialize Neo client: %w", err)
|
|
}
|
|
|
|
nnsContract, err := n.client.GetContractStateByID(1)
|
|
if err != nil {
|
|
return fmt.Errorf("get NNS contract state: %w", err)
|
|
}
|
|
|
|
n.invoker = invoker.New(n.client, nil)
|
|
n.nnsContract = nnsContract.Hash
|
|
|
|
return nil
|
|
}
|
|
|
|
// Close closes connections of multiSchemeClient.
|
|
func (n *NNS) Close() {
|
|
n.client.Close()
|
|
}
|
|
|
|
// ResolveContainerDomain looks up for NNS TXT records for the given container domain
|
|
// by calling `resolve` method of NNS contract. Returns the first record which represents
|
|
// valid container ID in a string format. Otherwise, returns an error.
|
|
//
|
|
// ResolveContainerDomain MUST NOT be called before successful Dial.
|
|
//
|
|
// See also https://docs.neo.org/docs/en-us/reference/nns.html.
|
|
func (n *NNS) ResolveContainerDomain(domain container.Domain) (cid.ID, error) {
|
|
item, err := unwrap.Item(n.invoker.Call(n.nnsContract, "resolve",
|
|
domain.Name()+"."+domain.Zone(), int64(nns.TXT),
|
|
))
|
|
if err != nil {
|
|
return cid.ID{}, fmt.Errorf("contract invocation: %w", err)
|
|
}
|
|
|
|
if _, ok := item.(stackitem.Null); !ok {
|
|
arr, ok := item.Value().([]stackitem.Item)
|
|
if !ok {
|
|
// unexpected for types from stackitem package
|
|
return cid.ID{}, errors.New("invalid cast to stack item slice")
|
|
}
|
|
|
|
var id cid.ID
|
|
|
|
for i := range arr {
|
|
bs, err := arr[i].TryBytes()
|
|
if err != nil {
|
|
return cid.ID{}, fmt.Errorf("convert array item to byte slice: %w", err)
|
|
}
|
|
|
|
err = id.DecodeString(string(bs))
|
|
if err == nil {
|
|
return id, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return cid.ID{}, errNotFound
|
|
}
|
|
|
|
// ResolveContractHash looks up for NNS TXT records for the given container domain
|
|
// by calling `resolve` method of NNS contract. Returns the first record which represents
|
|
// valid contract hash 20 bytes long unsigned integer. Otherwise, returns an error.
|
|
//
|
|
// ResolveContractHash MUST NOT be called before successful Dial.
|
|
//
|
|
// See also https://docs.neo.org/docs/en-us/reference/nns.html.
|
|
func (n *NNS) ResolveContractHash(domain container.Domain) (util.Uint160, error) {
|
|
item, err := unwrap.Item(n.invoker.Call(n.nnsContract, "resolve",
|
|
domain.Name()+"."+domain.Zone(), int64(nns.TXT),
|
|
))
|
|
if err != nil {
|
|
return util.Uint160{}, fmt.Errorf("contract invocation: %w", err)
|
|
}
|
|
|
|
if _, ok := item.(stackitem.Null); !ok {
|
|
arr, ok := item.Value().([]stackitem.Item)
|
|
if !ok {
|
|
// unexpected for types from stackitem package
|
|
return util.Uint160{}, errors.New("invalid cast to stack item slice")
|
|
}
|
|
|
|
for i := range arr {
|
|
recordValue, err := arr[i].TryBytes()
|
|
if err != nil {
|
|
return util.Uint160{}, fmt.Errorf("convert array item to byte slice: %w", err)
|
|
}
|
|
|
|
strRecordValue := string(recordValue)
|
|
scriptHash, err := address.StringToUint160(strRecordValue)
|
|
if err == nil {
|
|
return scriptHash, nil
|
|
}
|
|
scriptHash, err = util.Uint160DecodeStringLE(strRecordValue)
|
|
if err == nil {
|
|
return scriptHash, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return util.Uint160{}, errNotFound
|
|
}
|