git.frostfs.info/TrueCloudLab/frostfs-sdk-go@v0.0.0-20241022124111-5361f0ecebd3/ns/nns.go (about)

     1  package ns
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/url"
     8  
     9  	"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
    10  	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
    11  	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
    12  	"github.com/nspcc-dev/neo-go/pkg/core/state"
    13  	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
    14  	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
    15  	"github.com/nspcc-dev/neo-go/pkg/rpcclient"
    16  	"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
    17  	"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
    18  	"github.com/nspcc-dev/neo-go/pkg/util"
    19  	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
    20  )
    21  
    22  // multiSchemeClient unites invoker.RPCInvoke and common interface of
    23  // rpcclient.Client and rpcclient.WSClient.
    24  type multiSchemeClient interface {
    25  	invoker.RPCInvoke
    26  	// Init turns client to "ready-to-work" state.
    27  	Init() error
    28  	// Close closes connections.
    29  	Close()
    30  	// GetContractStateByID returns state of the NNS contract on 1 input.
    31  	GetContractStateByID(int32) (*state.Contract, error)
    32  }
    33  
    34  // NNS looks up FrostFS names using Neo Name Service.
    35  //
    36  // Instances are created with a variable declaration. Before work, the connection
    37  // to the NNS server MUST be established using Dial method.
    38  type NNS struct {
    39  	nnsContract util.Uint160
    40  	client      multiSchemeClient
    41  
    42  	invoker interface {
    43  		Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error)
    44  	}
    45  }
    46  
    47  // Dial connects to the address of the NNS server. If fails, the instance
    48  // MUST NOT be used.
    49  //
    50  // If URL address scheme is 'ws' or 'wss', then WebSocket protocol is used,
    51  // otherwise HTTP.
    52  func (n *NNS) Dial(address string) error {
    53  	var err error
    54  
    55  	uri, err := url.Parse(address)
    56  	if err == nil && (uri.Scheme == "ws" || uri.Scheme == "wss") {
    57  		n.client, err = rpcclient.NewWS(context.Background(), address, rpcclient.WSOptions{})
    58  		if err != nil {
    59  			return fmt.Errorf("create Neo WebSocket client: %w", err)
    60  		}
    61  	} else {
    62  		n.client, err = rpcclient.New(context.Background(), address, rpcclient.Options{})
    63  		if err != nil {
    64  			return fmt.Errorf("create Neo HTTP client: %w", err)
    65  		}
    66  	}
    67  
    68  	if err = n.client.Init(); err != nil {
    69  		return fmt.Errorf("initialize Neo client: %w", err)
    70  	}
    71  
    72  	nnsContract, err := n.client.GetContractStateByID(1)
    73  	if err != nil {
    74  		return fmt.Errorf("get NNS contract state: %w", err)
    75  	}
    76  
    77  	n.invoker = invoker.New(n.client, nil)
    78  	n.nnsContract = nnsContract.Hash
    79  
    80  	return nil
    81  }
    82  
    83  // Close closes connections of multiSchemeClient.
    84  func (n *NNS) Close() {
    85  	n.client.Close()
    86  }
    87  
    88  // ResolveContainerDomain looks up for NNS TXT records for the given container domain
    89  // by calling `resolve` method of NNS contract. Returns the first record which represents
    90  // valid container ID in a string format. Otherwise, returns an error.
    91  //
    92  // ResolveContainerDomain MUST NOT be called before successful Dial.
    93  //
    94  // See also https://docs.neo.org/docs/en-us/reference/nns.html.
    95  func (n *NNS) ResolveContainerDomain(domain container.Domain) (cid.ID, error) {
    96  	item, err := unwrap.Item(n.invoker.Call(n.nnsContract, "resolve",
    97  		domain.Name()+"."+domain.Zone(), int64(nns.TXT),
    98  	))
    99  	if err != nil {
   100  		return cid.ID{}, fmt.Errorf("contract invocation: %w", err)
   101  	}
   102  
   103  	if _, ok := item.(stackitem.Null); !ok {
   104  		arr, ok := item.Value().([]stackitem.Item)
   105  		if !ok {
   106  			// unexpected for types from stackitem package
   107  			return cid.ID{}, errors.New("invalid cast to stack item slice")
   108  		}
   109  
   110  		var id cid.ID
   111  
   112  		for i := range arr {
   113  			bs, err := arr[i].TryBytes()
   114  			if err != nil {
   115  				return cid.ID{}, fmt.Errorf("convert array item to byte slice: %w", err)
   116  			}
   117  
   118  			err = id.DecodeString(string(bs))
   119  			if err == nil {
   120  				return id, nil
   121  			}
   122  		}
   123  	}
   124  
   125  	return cid.ID{}, errNotFound
   126  }
   127  
   128  // ResolveContractHash looks up for NNS TXT records for the given container domain
   129  // by calling `resolve` method of NNS contract. Returns the first record which represents
   130  // valid contract hash 20 bytes long unsigned integer. Otherwise, returns an error.
   131  //
   132  // ResolveContractHash MUST NOT be called before successful Dial.
   133  //
   134  // See also https://docs.neo.org/docs/en-us/reference/nns.html.
   135  func (n *NNS) ResolveContractHash(domain container.Domain) (util.Uint160, error) {
   136  	item, err := unwrap.Item(n.invoker.Call(n.nnsContract, "resolve",
   137  		domain.Name()+"."+domain.Zone(), int64(nns.TXT),
   138  	))
   139  	if err != nil {
   140  		return util.Uint160{}, fmt.Errorf("contract invocation: %w", err)
   141  	}
   142  
   143  	if _, ok := item.(stackitem.Null); !ok {
   144  		arr, ok := item.Value().([]stackitem.Item)
   145  		if !ok {
   146  			// unexpected for types from stackitem package
   147  			return util.Uint160{}, errors.New("invalid cast to stack item slice")
   148  		}
   149  
   150  		for i := range arr {
   151  			recordValue, err := arr[i].TryBytes()
   152  			if err != nil {
   153  				return util.Uint160{}, fmt.Errorf("convert array item to byte slice: %w", err)
   154  			}
   155  
   156  			strRecordValue := string(recordValue)
   157  			scriptHash, err := address.StringToUint160(strRecordValue)
   158  			if err == nil {
   159  				return scriptHash, nil
   160  			}
   161  			scriptHash, err = util.Uint160DecodeStringLE(strRecordValue)
   162  			if err == nil {
   163  				return scriptHash, nil
   164  			}
   165  		}
   166  	}
   167  
   168  	return util.Uint160{}, errNotFound
   169  }