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 }