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  }