github.com/mavryk-network/mvgo@v1.19.9/internal/compose/alpha/parse.go (about)

     1  // Copyright (c) 2023 Blockwatch Data Inc.
     2  // Author: alex@blockwatch.cc, abdul@blockwatch.cc
     3  
     4  package alpha
     5  
     6  import (
     7  	"encoding"
     8  	"encoding/hex"
     9  	"encoding/json"
    10  	"fmt"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/mavryk-network/mvgo/contract/bind"
    16  	"github.com/mavryk-network/mvgo/internal/compose"
    17  	"github.com/mavryk-network/mvgo/mavryk"
    18  	"github.com/mavryk-network/mvgo/micheline"
    19  
    20  	"github.com/pkg/errors"
    21  )
    22  
    23  func ParseScript(ctx compose.Context, task Task) (*micheline.Script, error) {
    24  	var (
    25  		script *micheline.Script
    26  		err    error
    27  	)
    28  	if task.Script.ValueSource.IsUsed() {
    29  		script, err = loadSource[micheline.Script](ctx, task.Script.ValueSource)
    30  		if err != nil {
    31  			return nil, errors.Wrap(err, "loading script")
    32  		}
    33  	} else {
    34  		var (
    35  			code  *micheline.Code
    36  			store *micheline.Prim
    37  		)
    38  		if task.Script.Code == nil || !task.Script.Code.ValueSource.IsUsed() {
    39  			return nil, fmt.Errorf("missing script code source")
    40  		}
    41  		code, err = loadSource[micheline.Code](ctx, task.Script.Code.ValueSource)
    42  		if err != nil {
    43  			return nil, errors.Wrap(err, "loading script code")
    44  		}
    45  		if task.Script.Storage == nil || (!task.Script.Storage.ValueSource.IsUsed() && task.Script.Storage.Args == nil) {
    46  			return nil, fmt.Errorf("missing initial storage source")
    47  		}
    48  		if task.Script.Storage.Args != nil {
    49  			// processed below
    50  			store = &micheline.Prim{}
    51  		} else {
    52  			store, err = loadSource[micheline.Prim](ctx, task.Script.Storage.ValueSource)
    53  			if err != nil {
    54  				return nil, errors.Wrap(err, "loading initial storage")
    55  			}
    56  		}
    57  		script = &micheline.Script{
    58  			Code:    *code,
    59  			Storage: *store,
    60  		}
    61  	}
    62  	if !script.IsValid() {
    63  		return nil, fmt.Errorf("invalid script")
    64  	}
    65  
    66  	// Note: code patching is not supported
    67  
    68  	// patch storage
    69  	if task.Script.Storage != nil {
    70  		// ctx.Log.Infof("Storage Type: %s", script.Code.Storage.Dump())
    71  		// ctx.Log.Infof("Original Storage: %s", script.Storage.Dump())
    72  		for _, p := range task.Script.Storage.Patch {
    73  			if p.Path == nil {
    74  				ctx.Log.Debugf("Resolving storage path for %q...", *p.Key)
    75  				idx, ok := script.StorageType().LabelIndex(*p.Key)
    76  				if !ok {
    77  					return nil, fmt.Errorf("storage key %q not found", *p.Key)
    78  				}
    79  				s := idxToPath(idx)
    80  				p.Path = &s
    81  			}
    82  			ctx.Log.Debugf("Patching script storage...")
    83  			err = patch(ctx, p, func(path string, oc micheline.OpCode, prim micheline.Prim) error {
    84  				ctx.Log.Debugf("> path=%s type=%s val=%s", path, oc, prim.Dump())
    85  				return script.Storage.SetPath(path, prim)
    86  			})
    87  			if err != nil {
    88  				return nil, errors.Wrap(err, "patching storage")
    89  			}
    90  		}
    91  		if task.Script.Storage.Args != nil {
    92  			// replace variables in args
    93  			if _, _, err := resolveArgs(ctx, task.Script.Storage.Args); err != nil {
    94  				return nil, err
    95  			}
    96  			// map args to storage spec
    97  			// ctx.Log.Infof("Args: %#v", task.Script.Storage.Args)
    98  			// ctx.Log.Infof("Typedef: %s", script.StorageType().Typedef(""))
    99  			script.Storage, err = script.StorageType().Typedef("").Marshal(task.Script.Storage.Args, false)
   100  			if err != nil {
   101  				return nil, err
   102  			}
   103  			// ctx.Log.Infof("Gen: %s", script.Storage.Dump())
   104  			// ctx.Log.Infof("Gen: %#v", script.Storage)
   105  			// buf, err := script.Storage.MarshalBinary()
   106  			// if err != nil {
   107  			// 	return nil, err
   108  			// }
   109  			// ctx.Log.Infof("Gen: %s", mavryk.HexBytes(buf))
   110  		}
   111  	}
   112  	if !script.Storage.IsValid() {
   113  		return nil, fmt.Errorf("invalid storage %s", script.Storage.Dump())
   114  	}
   115  
   116  	return script, nil
   117  }
   118  
   119  func ParseParams(ctx compose.Context, task Task) (*micheline.Prim, error) {
   120  	var (
   121  		params *micheline.Prim
   122  		err    error
   123  	)
   124  	if task.Params.ValueSource.IsUsed() {
   125  		params, err = loadSource[micheline.Prim](ctx, task.Params.ValueSource)
   126  		if err != nil {
   127  			return nil, err
   128  		}
   129  	}
   130  	for _, p := range task.Params.Patch {
   131  		if p.Path == nil {
   132  			ctx.Log.Debugf("Resolving destination script for %s...", task.Destination)
   133  			// load script
   134  			addr, err := mavryk.ParseAddress(task.Destination)
   135  			if err != nil {
   136  				return nil, err
   137  			}
   138  			script, err := ctx.ResolveScript(addr)
   139  			if err != nil {
   140  				return nil, err
   141  			}
   142  			ctx.Log.Debugf("Resolving storage path for %q...", *p.Key)
   143  			idx, ok := script.StorageType().LabelIndex(*p.Key)
   144  			if !ok {
   145  				return nil, fmt.Errorf("storage key %q not found", *p.Key)
   146  			}
   147  			s := idxToPath(idx)
   148  			p.Path = &s
   149  		}
   150  
   151  		ctx.Log.Debugf("Patching params...")
   152  		err = patch(ctx, p, func(path string, oc micheline.OpCode, prim micheline.Prim) error {
   153  			ctx.Log.Debugf("> path=%s type=%s val=%s", path, oc, prim.Dump())
   154  			return params.SetPath(path, prim)
   155  		})
   156  		if err != nil {
   157  			return nil, errors.Wrap(err, "patching params")
   158  		}
   159  	}
   160  	if task.Params.Args != nil {
   161  		// replace variables in args
   162  		if _, _, err := resolveArgs(ctx, task.Params.Args); err != nil {
   163  			return nil, err
   164  		}
   165  		addr, err := ctx.ResolveAddress(task.Destination)
   166  		if err != nil {
   167  			return nil, err
   168  		}
   169  		// load script
   170  		ctx.Log.Debugf("Resolving destination script for %s...", addr)
   171  		script, err := ctx.ResolveScript(addr)
   172  		if err != nil {
   173  			return nil, err
   174  		}
   175  		// map args to entrypoint spec
   176  		eps, err := script.Entrypoints(true)
   177  		if err != nil {
   178  			return nil, err
   179  		}
   180  		ep, ok := eps[task.Params.Entrypoint]
   181  		if !ok {
   182  			return nil, fmt.Errorf("entrypoint %q not found, have %#v", task.Params.Entrypoint, eps)
   183  		}
   184  		// marshal to prim tree
   185  		// Note: be mindful of the way entrypoint typedefs are structured:
   186  		// - 1 arg: use scalar value in ep.Typedef[0]
   187  		// - >1 arg: use entire list in ep.Typedef but wrap into struct
   188  		ctx.Log.Debugf("Marshaling params...")
   189  		// ctx.Log.Debugf("EP prim=%s", ep.Prim.Dump())
   190  		// ctx.Log.Debugf("EP type=%s", ep.Typedef)
   191  		typ := ep.Typedef[0]
   192  		if len(ep.Typedef) > 1 {
   193  			typ = micheline.Typedef{
   194  				Name: micheline.CONST_ENTRYPOINT,
   195  				Type: micheline.TypeStruct,
   196  				Args: ep.Typedef,
   197  			}
   198  		}
   199  		// ctx.Log.Debugf("USE type=%s", typ)
   200  		prim, err := typ.Marshal(task.Params.Args, true)
   201  		if err != nil {
   202  			return nil, errors.Wrap(err, "marshal params")
   203  		}
   204  		ctx.Log.Debugf("Result %s", prim.Dump())
   205  		params = &prim
   206  	}
   207  	return params, nil
   208  }
   209  
   210  func loadSource[T any](ctx compose.Context, src ValueSource) (val *T, err error) {
   211  	switch {
   212  	case src.Value != "":
   213  		val = new(T)
   214  		if !isHex(src.Value) {
   215  			err = json.Unmarshal([]byte(src.Value), val)
   216  		} else {
   217  			var buf []byte
   218  			buf, err = hex.DecodeString(src.Value)
   219  			if err == nil {
   220  				x := any(val)
   221  				if u, ok := x.(encoding.BinaryUnmarshaler); ok {
   222  					err = u.UnmarshalBinary(buf)
   223  				} else {
   224  					err = fmt.Errorf("type %T does not implement encoding.BinaryUnmarshaler", val)
   225  				}
   226  			}
   227  		}
   228  	case src.File != "":
   229  		fname := filepath.Join(ctx.Filepath(), src.File)
   230  		val, err = compose.ReadJsonFile[T](fname)
   231  	case src.Url != "":
   232  		val, err = compose.Fetch[T](ctx, src.Url)
   233  	default:
   234  		err = fmt.Errorf("invalid source")
   235  	}
   236  	return
   237  }
   238  
   239  func patch(ctx compose.Context, p Patch, updater func(string, micheline.OpCode, micheline.Prim) error) error {
   240  	opCode, err := micheline.ParseOpCode(p.Type)
   241  	if err != nil {
   242  		return err
   243  	}
   244  	if !opCode.IsTypeCode() {
   245  		return fmt.Errorf("%s is not a valid type", p.Type)
   246  	}
   247  	str, err := ctx.ResolveString(*p.Value)
   248  	if err != nil {
   249  		return err
   250  	}
   251  	val, err := compose.ParseValue(opCode, str)
   252  	if err != nil {
   253  		return err
   254  	}
   255  	prim, err := bind.MarshalPrim(val, p.Optimized)
   256  	if err != nil {
   257  		return err
   258  	}
   259  	return updater(*p.Path, opCode, prim)
   260  }
   261  
   262  func idxToPath(idx []int) string {
   263  	if len(idx) == 0 {
   264  		return ""
   265  	}
   266  	var b strings.Builder
   267  	b.WriteString(strconv.Itoa(idx[0]))
   268  	for _, v := range idx[1:] {
   269  		b.WriteByte('/')
   270  		b.WriteString(strconv.Itoa(v))
   271  	}
   272  	return b.String()
   273  }
   274  
   275  func resolveArgs(ctx compose.Context, args any) (string, bool, error) {
   276  	if args == nil {
   277  		return "", false, nil
   278  	}
   279  	switch val := args.(type) {
   280  	case map[string]any:
   281  		for n, v := range val {
   282  			s, ok, err := resolveArgs(ctx, v)
   283  			if err != nil {
   284  				return "", false, fmt.Errorf("arg %s: %v", n, err)
   285  			}
   286  			// try convert key (may be composite key)
   287  			if newKey, ok := ctx.ResolveNestedVars(n); ok {
   288  				delete(val, n)
   289  				n = newKey
   290  				val[n] = v
   291  			}
   292  			if ok {
   293  				val[n] = s
   294  			}
   295  		}
   296  	case []any:
   297  		for i, v := range val {
   298  			s, ok, err := resolveArgs(ctx, v)
   299  			if err != nil {
   300  				return "", false, fmt.Errorf("arg %d: %v", i, err)
   301  			}
   302  			if ok {
   303  				val[i] = s
   304  			}
   305  		}
   306  	case string:
   307  		if s, err := ctx.ResolveString(val); err != nil {
   308  			return "", false, err
   309  		} else {
   310  			return s, true, nil
   311  		}
   312  	case int:
   313  		// skip
   314  	case bool:
   315  		// skip
   316  	default:
   317  		return "", false, fmt.Errorf("unsupported arg type %T", args)
   318  	}
   319  	return "", false, nil
   320  }
   321  
   322  func isHex(s string) bool {
   323  	_, err := hex.DecodeString(s)
   324  	return err == nil
   325  }
   326  
   327  // func isJson(s string) bool {
   328  // 	if len(s) == 0 {
   329  // 		return true
   330  // 	}
   331  // 	switch s[0] {
   332  // 	case '{', '[', '"', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-':
   333  // 		return true
   334  // 	default:
   335  // 		return false
   336  // 	}
   337  // }