frostfs-sdk-go/ns/nns.go
Leonard Lyubich d51d18d5f3 [#179] ns: Select Neo connection scheme dynamically in NNS.Dial
WebSocket protocol is optionally supported by Neo servers.

Make `NNS.Dial` to select between HTTP and WebSocket connections
according to URL address scheme.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-13 10:46:27 +03:00

165 lines
5 KiB
Go

package ns
import (
"context"
"errors"
"fmt"
"net/url"
nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns"
"github.com/nspcc-dev/neo-go/pkg/core/state"
neoclient "github.com/nspcc-dev/neo-go/pkg/rpc/client"
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
)
// NNS looks up NeoFS 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
neoClient neoClient
}
// represents virtual connection to Neo network used by NNS.Dial.
type neoClient interface {
// calls specified method of the Neo smart contract with provided parameters.
call(contract util.Uint160, method string, prm []smartcontract.Parameter) (*result.Invoke, error)
}
// implements neoClient using Neo HTTP client.
//
// note: see NNS.Dial to realize why this isn't defined as type wrapper like neoWebSocket.
type neoHTTP struct {
*neoclient.Client
}
func (x *neoHTTP) call(contract util.Uint160, method string, prm []smartcontract.Parameter) (*result.Invoke, error) {
return x.Client.InvokeFunction(contract, method, prm, nil)
}
// implements neoClient using Neo WebSocket client.
type neoWebSocket neoclient.WSClient
func (x *neoWebSocket) call(contract util.Uint160, method string, prm []smartcontract.Parameter) (*result.Invoke, error) {
return (*neoclient.WSClient)(x).InvokeFunction(contract, method, prm, nil)
}
// Dial connects to the address of the NNS server. If fails, the instance
// MUST NOT be used.
//
// If URL address scheme is 'ws', then WebSocket protocol is used, otherwise HTTP.
func (n *NNS) Dial(address string) error {
// multiSchemeClient unites neoClient and common interface of
// neoclient.Client and neoclient.WSClient. Interface is anonymous
// according to assumption that common interface of these client types
// is not required by design and may diverge with changes.
var multiSchemeClient interface {
neoClient
// Init turns client to "ready-to-work" state.
Init() error
// GetContractStateByID returns state of the NNS contract on 1 input.
GetContractStateByID(int32) (*state.Contract, error)
}
uri, err := url.Parse(address)
if err == nil && uri.Scheme == "ws" {
cWebSocket, err := neoclient.NewWS(context.Background(), address, neoclient.Options{})
if err != nil {
return fmt.Errorf("create Neo WebSocket client: %w", err)
}
multiSchemeClient = (*neoWebSocket)(cWebSocket)
} else {
cHTTP, err := neoclient.New(context.Background(), address, neoclient.Options{})
if err != nil {
return fmt.Errorf("create Neo HTTP client: %w", err)
}
// if neoHTTP is defined as type wrapper
// type neoHTTP neoclient.Client
// then next assignment causes compilation error
// multiSchemeClient = (*neoHTTP)(cHTTP)
multiSchemeClient = &neoHTTP{
Client: cHTTP,
}
}
if err = multiSchemeClient.Init(); err != nil {
return fmt.Errorf("initialize Neo client: %w", err)
}
nnsContract, err := multiSchemeClient.GetContractStateByID(1)
if err != nil {
return fmt.Errorf("get NNS contract state: %w", err)
}
n.neoClient = multiSchemeClient
n.nnsContract = nnsContract.Hash
return nil
}
// ResolveContainerName looks up for NNS TXT records for the given container name
// by calling `resolve` method of NNS contract. Returns the first record which represents
// valid container ID in a string format. Otherwise, returns an error.
//
// ResolveContainerName MUST NOT be called before successful Dial.
//
// See also https://docs.neo.org/docs/en-us/reference/nns.html.
func (n *NNS) ResolveContainerName(name string) (*cid.ID, error) {
res, err := n.neoClient.call(n.nnsContract, "resolve", []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: name + ".container",
},
{
Type: smartcontract.IntegerType,
Value: int64(nns.TXT),
},
})
if err != nil {
return nil, fmt.Errorf("invoke NNS contract: %w", err)
}
if res.State != vm.HaltState.String() {
return nil, fmt.Errorf("NNS contract fault exception: %s", res.FaultException)
} else if len(res.Stack) == 0 {
return nil, errors.New("empty stack in invocation result")
}
itemArr, err := res.Stack[len(res.Stack)-1].Convert(stackitem.ArrayT) // top stack element is last in the array
if err != nil {
return nil, fmt.Errorf("convert stack item to %s", stackitem.ArrayT)
}
if _, ok := itemArr.(stackitem.Null); !ok {
arr, ok := itemArr.Value().([]stackitem.Item)
if !ok {
// unexpected for types from stackitem package
return nil, 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 nil, fmt.Errorf("convert array item to byte slice: %w", err)
}
err = id.Parse(string(bs))
if err == nil {
return &id, nil
}
}
}
return nil, errNotFound
}