github.com/mavryk-network/mvgo@v1.19.9/contract/tz16.go (about) 1 // Copyright (c) 2020-2022 Blockwatch Data Inc. 2 // Author: alex@blockwatch.cc 3 package contract 4 5 import ( 6 "bytes" 7 "context" 8 "crypto/sha256" 9 "encoding/hex" 10 "encoding/json" 11 "fmt" 12 "hash" 13 "io" 14 "net/http" 15 "net/url" 16 "strconv" 17 "strings" 18 19 "github.com/mavryk-network/mvgo/mavryk" 20 "github.com/mavryk-network/mvgo/micheline" 21 "github.com/mavryk-network/mvgo/rpc" 22 ) 23 24 // Represents Tzip16 contract metadata 25 type Tz16 struct { 26 Name string `json:"name"` 27 Description string `json:"description,omitempty"` 28 Version string `json:"version,omitempty"` 29 License *Tz16License `json:"license,omitempty"` 30 Authors []string `json:"authors,omitempty"` 31 Homepage string `json:"homepage,omitempty"` 32 Source *Tz16Source `json:"source,omitempty"` 33 Interfaces []string `json:"interfaces,omitempty"` 34 Errors []Tz16Error `json:"errors,omitempty"` 35 Views []Tz16View `json:"views,omitempty"` 36 } 37 38 type Tz16License struct { 39 Name string `json:"name"` 40 Details string `json:"details,omitempty"` 41 } 42 43 func (l *Tz16License) UnmarshalJSON(data []byte) error { 44 if len(data) == 0 { 45 return nil 46 } 47 switch data[0] { 48 case '"': 49 name, err := strconv.Unquote(string(data)) 50 l.Name = name 51 return err 52 case '{': 53 type alias Tz16License 54 return json.Unmarshal(data, (*alias)(l)) 55 default: 56 return fmt.Errorf("invalid license (not string or object)") 57 } 58 } 59 60 type Tz16Source struct { 61 Tools []string `json:"tools"` 62 Location string `json:"location,omitempty"` 63 } 64 65 type Tz16Error struct { 66 Error *micheline.Prim `json:"error,omitempty"` 67 Expansion *micheline.Prim `json:"expansion,omitempty"` 68 Languages []string `json:"languages,omitempty"` 69 View string `json:"view,omitempty"` 70 } 71 72 type Tz16View struct { 73 Name string `json:"name"` 74 Description string `json:"description,omitempty"` 75 Pure bool `json:"pure,omitempty"` 76 Implementations []Tz16ViewImpl `json:"implementations,omitempty"` 77 } 78 79 type Tz16ViewImpl struct { 80 Storage *Tz16StorageView `json:"michelsonStorageView,omitempty"` 81 Rest *Tz16RestView `json:"restApiQuery,omitempty"` 82 } 83 84 type Tz16StorageView struct { 85 ParamType micheline.Prim `json:"parameter"` 86 ReturnType micheline.Prim `json:"returnType"` 87 Code micheline.Prim `json:"code"` 88 Annotations []Tz16CodeAnnotation `json:"annotations,omitempty"` 89 Version string `json:"version,omitempty"` 90 } 91 92 type Tz16CodeAnnotation struct { 93 Name string `json:"name"` 94 Description string `json:"description"` 95 } 96 97 type Tz16RestView struct { 98 SpecUri string `json:"specificationUri"` 99 BaseUri string `json:"baseUri"` 100 Path string `json:"path"` 101 Method string `json:"method"` 102 } 103 104 func (t Tz16) Validate() []error { 105 // TODO: json schema validator 106 return nil 107 } 108 109 func (t Tz16) HasView(name string) bool { 110 for _, v := range t.Views { 111 if v.Name == name { 112 return true 113 } 114 } 115 return false 116 } 117 118 func (t Tz16) GetView(name string) Tz16View { 119 for _, v := range t.Views { 120 if v.Name == name { 121 return v 122 } 123 } 124 return Tz16View{} 125 } 126 127 func (v *Tz16View) Run(ctx context.Context, contract *Contract, args micheline.Prim) (micheline.Prim, error) { 128 if len(v.Implementations) == 0 || v.Implementations[0].Storage == nil { 129 return micheline.InvalidPrim, fmt.Errorf("missing storage view impl") 130 } 131 return v.Implementations[0].Storage.Run(ctx, contract, args) 132 } 133 134 // Run executes the TZIP-16 off-chain view using script and storage from contract and 135 // passed args. Returns the result as primitive which matches the view's return type. 136 // Note this method does not check or patch the view code to replace illegal instructions 137 // or inject current context. 138 func (v *Tz16StorageView) Run(ctx context.Context, contract *Contract, args micheline.Prim) (micheline.Prim, error) { 139 // fill empty arguments 140 code := v.Code.Clone() 141 paramType := v.ParamType 142 if !paramType.IsValid() { 143 paramType = micheline.NewCode(micheline.T_UNIT) 144 if !args.IsValid() { 145 args = micheline.NewCode(micheline.D_UNIT) 146 } 147 code.Args = append(micheline.PrimList{micheline.NewCode(micheline.I_CDR)}, code.Args...) 148 } 149 150 // construct request 151 req := rpc.RunCodeRequest{ 152 ChainId: contract.rpc.ChainId, 153 Script: micheline.Code{ 154 Param: micheline.NewCode( 155 micheline.K_PARAMETER, 156 micheline.NewPairType(paramType, contract.script.Code.Storage.Args[0]), 157 ), 158 Storage: micheline.NewCode( 159 micheline.K_STORAGE, 160 micheline.NewCode(micheline.T_OPTION, v.ReturnType), 161 ), 162 Code: micheline.NewCode( 163 micheline.K_CODE, 164 micheline.NewSeq( 165 micheline.NewCode(micheline.I_CAR), 166 v.Code, 167 micheline.NewCode(micheline.I_SOME), 168 micheline.NewCode(micheline.I_NIL, micheline.NewCode(micheline.T_OPERATION)), 169 micheline.NewCode(micheline.I_PAIR), 170 ), 171 ), 172 }, 173 Input: micheline.NewPair(args, *contract.store), 174 Storage: micheline.NewCode(micheline.D_NONE), 175 Amount: mavryk.N(0), 176 Balance: mavryk.N(0), 177 } 178 var resp rpc.RunCodeResponse 179 if err := contract.rpc.RunCode(ctx, rpc.Head, req, &resp); err != nil { 180 return micheline.InvalidPrim, err 181 } 182 183 // strip the extra D_SOME 184 return resp.Storage.Args[0], nil 185 } 186 187 func (c *Contract) ResolveTz16Uri(ctx context.Context, uri string, result interface{}, checksum []byte) error { 188 protoIdx := strings.Index(uri, ":") 189 if protoIdx < 0 { 190 return fmt.Errorf("malformed tzip16 uri %q", uri) 191 } 192 193 switch uri[:protoIdx] { 194 case "mavryk-storage": 195 return c.resolveStorageUri(ctx, uri, result, checksum) 196 case "http", "https": 197 return c.resolveHttpUri(ctx, uri, result, checksum) 198 case "sha256": 199 parts := strings.Split(strings.TrimPrefix(uri, "sha256://"), "/") 200 checksum, err := hex.DecodeString(parts[0][2:]) 201 if err != nil { 202 return fmt.Errorf("invalid sha256 checksum: %v", err) 203 } 204 if len(parts) < 2 { 205 return fmt.Errorf("malformed tzip16 uri %q", uri) 206 } 207 uri, err = url.QueryUnescape(parts[1]) 208 if err != nil { 209 return fmt.Errorf("malformed tzip16 uri %q: %v", parts[1], err) 210 } 211 return c.ResolveTz16Uri(ctx, uri, result, checksum) 212 case "ipfs": 213 return c.resolveIpfsUri(ctx, uri, result, checksum) 214 default: 215 return fmt.Errorf("unsupported tzip16 protocol %q", uri[:protoIdx]) 216 } 217 } 218 219 func (c *Contract) resolveStorageUri(ctx context.Context, uri string, result interface{}, checksum []byte) error { 220 if !strings.HasPrefix(uri, "mavryk-storage:") { 221 return fmt.Errorf("invalid tzip16 storage uri prefix: %q", uri) 222 } 223 224 // prefix is either `mavryk-storage:` or `mavryk-storage://` 225 uri = strings.TrimPrefix(uri, "mavryk-storage://") 226 uri = strings.TrimPrefix(uri, "mavryk-storage:") 227 parts := strings.SplitN(uri, "/", 2) 228 229 // resolve bigmap and key to read 230 var ( 231 key string 232 id int64 233 ok bool 234 err error 235 con *Contract 236 ) 237 if len(parts) == 1 { 238 // same contract 239 con = c 240 id, ok = c.script.Bigmaps()["metadata"] 241 if !ok { 242 return fmt.Errorf("%s: missing metadata bigmap", c.addr) 243 } 244 key = parts[0] 245 } else { 246 // other contract 247 addr, err := mavryk.ParseAddress(parts[0]) 248 if err != nil { 249 return fmt.Errorf("malformed tzip16 uri %q: %v", uri, err) 250 } 251 con = NewContract(addr, c.rpc) 252 if err := con.Resolve(ctx); err != nil { 253 return fmt.Errorf("cannot resolve %s: %v", addr, err) 254 } 255 id, ok = con.script.Bigmaps()["metadata"] 256 if !ok { 257 return fmt.Errorf("%s: missing metadata bigmap", addr) 258 } 259 key = parts[1] 260 } 261 262 // unescape 263 key, err = url.QueryUnescape(key) 264 if err != nil { 265 return fmt.Errorf("malformed tzip16 uri %q: %v", uri, err) 266 } 267 hash := (micheline.Key{ 268 Type: micheline.NewType(micheline.NewPrim(micheline.T_STRING)), 269 StringKey: key, 270 }).Hash() 271 272 prim, err := con.rpc.GetActiveBigmapValue(ctx, id, hash) 273 if err != nil { 274 return err 275 } 276 if !prim.IsValid() || prim.Type != micheline.PrimBytes { 277 return fmt.Errorf("Unexpected storage value type %s %q", prim.Type, prim.Dump()) 278 } 279 280 // unpack JSON data 281 if l := len(prim.Bytes); l > 0 && prim.Bytes[0] == '{' && prim.Bytes[l-1] == '}' { 282 if checksum != nil { 283 hash := sha256.Sum256(prim.Bytes) 284 if !bytes.Equal(hash[:], checksum) { 285 return fmt.Errorf("checksum mismatch") 286 } 287 } 288 289 return json.Unmarshal(prim.Bytes, result) 290 } 291 292 // try recurse if content looks like another URI 293 return con.ResolveTz16Uri(ctx, string(prim.Bytes), result, checksum) 294 } 295 296 func (c *Contract) resolveHttpUri(ctx context.Context, uri string, result interface{}, checksum []byte) error { 297 if !strings.HasPrefix(uri, "http") { 298 return fmt.Errorf("invalid tzip16 http uri prefix: %q", uri) 299 } 300 req, err := http.NewRequest(http.MethodGet, uri, nil) 301 if err != nil { 302 return err 303 } 304 req = req.WithContext(ctx) 305 req.Header.Add("Accept", "text/plain; charset=utf-8") 306 req.Header.Add("User-Agent", c.rpc.UserAgent) 307 308 resp, err := c.rpc.Client().Do(req) 309 if err != nil { 310 return err 311 } 312 defer func() { 313 io.Copy(io.Discard, resp.Body) 314 resp.Body.Close() 315 }() 316 if resp.StatusCode/100 != 2 { 317 return fmt.Errorf("GET %s: %d %s", uri, resp.StatusCode, resp.Status) 318 } 319 320 var reader io.Reader = resp.Body 321 var h hash.Hash 322 if checksum != nil { 323 h = sha256.New() 324 reader = io.TeeReader(reader, h) 325 } 326 327 err = json.NewDecoder(reader).Decode(result) 328 if err != nil { 329 return err 330 } 331 if checksum != nil && !bytes.Equal(h.Sum(nil), checksum) { 332 return fmt.Errorf("checksum mismatch") 333 } 334 return nil 335 } 336 337 func (c *Contract) resolveIpfsUri(ctx context.Context, uri string, result interface{}, checksum []byte) error { 338 if !strings.HasPrefix(uri, "ipfs://") { 339 return fmt.Errorf("invalid tzip16 ipfs uri prefix: %q", uri) 340 } 341 gateway := strings.TrimSuffix(strings.TrimPrefix(c.rpc.IpfsURL.String(), "https://"), "/") 342 gateway = "https://" + gateway + "/ipfs/" 343 uri = strings.Replace(uri, "ipfs://", gateway, 1) 344 return c.resolveHttpUri(ctx, uri, result, checksum) 345 }