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

     1  // Copyright (c) 2023 Blockwatch Data Inc.
     2  // Author: alex@blockwatch.cc, abdul@blockwatch.cc
     3  
     4  package compose
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"fmt"
    10  	"path/filepath"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/mavryk-network/mvgo/codec"
    15  	"github.com/mavryk-network/mvgo/mavryk"
    16  	"github.com/mavryk-network/mvgo/micheline"
    17  	"github.com/mavryk-network/mvgo/rpc"
    18  
    19  	"github.com/echa/log"
    20  )
    21  
    22  type Engine interface {
    23  	Clone(Context, []Op, CloneConfig) ([]byte, error)
    24  	Validate(Context, string) error
    25  	Run(Context, string) error
    26  }
    27  
    28  type Account struct {
    29  	Id         int
    30  	Address    mavryk.Address
    31  	PrivateKey mavryk.PrivateKey
    32  }
    33  
    34  type Context struct {
    35  	context.Context
    36  	BaseAccount  Account
    37  	MaxId        int
    38  	Accounts     map[mavryk.Address]Account
    39  	Contracts    map[mavryk.Address]*micheline.Script
    40  	Variables    map[string]string
    41  	Log          log.Logger
    42  	client       *rpc.Client // RPC client
    43  	url          string      // RPC service URL
    44  	apiKey       string      // RPC service API key
    45  	path         string      // current compose file path
    46  	resume       bool        // continue pipeline execution were we left off
    47  	mode         RunMode     // selected engine run mode
    48  	cache        *PipelineCache
    49  	savedLoggers [2]log.Logger
    50  }
    51  
    52  func NewContext(ctx context.Context) Context {
    53  	return Context{
    54  		Context:   ctx,
    55  		Accounts:  make(map[mavryk.Address]Account),
    56  		Contracts: make(map[mavryk.Address]*micheline.Script),
    57  		MaxId:     -1,
    58  		Variables: make(map[string]string),
    59  		Log:       log.Disabled,
    60  		cache:     NewCache(),
    61  	}
    62  }
    63  
    64  func (c *Context) WithLogger(l log.Logger) *Context {
    65  	c.Log = l
    66  	return c
    67  }
    68  
    69  func (c *Context) WithUrl(u string) *Context {
    70  	if !strings.HasPrefix(u, "http") {
    71  		u = "http://" + u
    72  	}
    73  	c.url = u
    74  	return c
    75  }
    76  
    77  func (c *Context) WithApiKey(k string) *Context {
    78  	c.apiKey = k
    79  	return c
    80  }
    81  
    82  func (c *Context) WithResume(b bool) *Context {
    83  	c.resume = b
    84  	return c
    85  }
    86  
    87  func (c *Context) WithMode(m RunMode) *Context {
    88  	c.mode = m
    89  	return c
    90  }
    91  
    92  func (c Context) ShouldResume() bool {
    93  	return c.resume
    94  }
    95  
    96  func (c Context) Filepath() string {
    97  	return c.path
    98  }
    99  
   100  func (c *Context) WithPath(p string) *Context {
   101  	c.path = p
   102  	return c
   103  }
   104  
   105  func (c *Context) WithBase(k string) *Context {
   106  	if k == "" {
   107  		return c
   108  	}
   109  	sk, err := mavryk.ParsePrivateKey(k)
   110  	if err != nil {
   111  		c.Log.Errorf("base key: %v", err)
   112  		return c
   113  	}
   114  	c.BaseAccount.Id = -1
   115  	c.BaseAccount.PrivateKey = sk
   116  	c.BaseAccount.Address = sk.Address()
   117  	c.AddVariable("base", c.BaseAccount.Address.String())
   118  	c.AddAccount(c.BaseAccount)
   119  	return c
   120  }
   121  
   122  func (c *Context) Cache() *PipelineCache {
   123  	return c.cache
   124  }
   125  
   126  func (c *Context) Init() (err error) {
   127  	if !c.BaseAccount.PrivateKey.IsValid() {
   128  		err = ErrNoBaseKey
   129  		return
   130  	}
   131  	c.Log.Infof("Using base account %s", c.BaseAccount.Address)
   132  	c.AddVariable("zero", mavryk.ZeroAddress.String())
   133  	c.AddVariable("burn", mavryk.BurnAddress.String())
   134  	c.client, err = rpc.NewClient(c.url, nil)
   135  	if err != nil {
   136  		return
   137  	}
   138  	c.client.ApiKey = c.apiKey
   139  	c.client.CloseConns = true // fix node EOF
   140  	err = c.client.Init(c.Context)
   141  	if err == nil {
   142  		c.Log.Infof("Using chain %s (%s) with %s blocks",
   143  			c.client.ChainId, c.client.Params.Network, c.client.Params.MinimalBlockDelay)
   144  	}
   145  	return
   146  }
   147  
   148  func (c *Context) AddVariable(key, val string) {
   149  	if key == "" {
   150  		return
   151  	}
   152  	c.Log.Debugf("Add var %s=%s", key, val)
   153  	c.Variables[CreateVariable(key)] = val
   154  }
   155  
   156  func (c *Context) AddAccount(acc Account) {
   157  	c.Log.Debugf("Add account %d=%s key=%s", acc.Id, acc.Address, acc.PrivateKey)
   158  	c.Accounts[acc.Address] = acc
   159  }
   160  
   161  func (c *Context) ResolveString(val any) (string, error) {
   162  	if val == nil {
   163  		return "", nil
   164  	}
   165  	v, ok := val.(string)
   166  	if !ok {
   167  		return "", fmt.Errorf("invalid type %T, expected string", val)
   168  	}
   169  	switch {
   170  	case IsTimeExpression(v):
   171  		if conv, err := ConvertTime(v); err != nil {
   172  			return "", err
   173  		} else {
   174  			return conv, nil
   175  		}
   176  	case IsVariable(v):
   177  		if val, ok := c.Variables[v]; ok {
   178  			return val, nil
   179  		} else {
   180  			return "", fmt.Errorf("undefined variable %s", v)
   181  		}
   182  	case IsFile(v):
   183  		fname := filepath.Join(c.path, v[1:])
   184  		val, err := ReadJsonFile[string](fname)
   185  		if err != nil {
   186  			return "", err
   187  		}
   188  		return *val, nil
   189  	default:
   190  		return v, nil
   191  	}
   192  }
   193  
   194  func (c *Context) ResolveAddress(val any) (a mavryk.Address, err error) {
   195  	if val == nil {
   196  		err = fmt.Errorf("missing value")
   197  		return
   198  	}
   199  	v, err := c.ResolveString(val)
   200  	if err != nil {
   201  		return
   202  	}
   203  	if v == "" {
   204  		err = fmt.Errorf("missing value")
   205  		return
   206  	}
   207  	a, err = mavryk.ParseAddress(v)
   208  	return
   209  }
   210  
   211  func (c *Context) ResolveInt64(val any) (i int64, err error) {
   212  	if val == nil {
   213  		err = fmt.Errorf("missing value")
   214  		return
   215  	}
   216  	switch v := val.(type) {
   217  	case int:
   218  		i = int64(v)
   219  	case int64:
   220  		i = v
   221  	case uint:
   222  		i = int64(v)
   223  	case uint64:
   224  		i = int64(v)
   225  	case string:
   226  		i, err = strconv.ParseInt(v, 10, 64)
   227  	default:
   228  		err = fmt.Errorf("invalid type %T for int64 value", val)
   229  	}
   230  	return
   231  }
   232  
   233  func (c *Context) ResolveZ(val any) (z mavryk.Z, err error) {
   234  	if val == nil {
   235  		err = fmt.Errorf("missing value")
   236  		return
   237  	}
   238  	switch v := val.(type) {
   239  	case int:
   240  		z = mavryk.NewZ(int64(v))
   241  	case int64:
   242  		z = mavryk.NewZ(v)
   243  	case uint:
   244  		z = mavryk.NewZ(int64(v))
   245  	case uint64:
   246  		z = mavryk.NewZ(int64(v))
   247  	case string:
   248  		v, err = c.ResolveString(v)
   249  		if err != nil {
   250  			return
   251  		}
   252  		z, err = mavryk.ParseZ(v)
   253  	default:
   254  		err = fmt.Errorf("invalid type %T for bigint value", val)
   255  	}
   256  	return
   257  }
   258  
   259  func (c *Context) ResolvePrivateKey(val any) (sk mavryk.PrivateKey, err error) {
   260  	v, ok := val.(string)
   261  	if !ok {
   262  		err = fmt.Errorf("invalid type %T, expected string", val)
   263  		return
   264  	}
   265  	if v == "" {
   266  		return c.BaseAccount.PrivateKey, nil
   267  	}
   268  	var addr mavryk.Address
   269  	addr, err = c.ResolveAddress(val)
   270  	if err != nil {
   271  		return
   272  	}
   273  	acc, ok := c.Accounts[addr]
   274  	if !ok {
   275  		err = ErrNoAccount
   276  		return
   277  	}
   278  	sk = acc.PrivateKey
   279  	return
   280  }
   281  
   282  func (c *Context) ResolveScript(addr mavryk.Address) (*micheline.Script, error) {
   283  	if s, ok := c.Contracts[addr]; ok {
   284  		return s, nil
   285  	}
   286  	script, err := c.client.GetNormalizedScript(c.Context, addr, "")
   287  	if err != nil {
   288  		return nil, err
   289  	}
   290  	script.Code.Code = micheline.InvalidPrim
   291  	script.Code.View = micheline.InvalidPrim
   292  	c.Contracts[addr] = script
   293  	return script, nil
   294  }
   295  
   296  func (c *Context) Send(op *codec.Op, opts *rpc.CallOptions) (*rpc.Receipt, error) {
   297  	if c.mode == RunModeSimulate {
   298  		key, err := opts.Signer.GetKey(c.Context, op.Source)
   299  		if err != nil {
   300  			return nil, err
   301  		}
   302  		if err := c.client.Complete(c.Context, op, key); err != nil {
   303  			return nil, err
   304  		}
   305  		return c.client.Simulate(c.Context, op, opts)
   306  	}
   307  	rcpt, err := c.client.Send(c.Context, op, opts)
   308  	if err != nil {
   309  		return nil, err
   310  	}
   311  	if !rcpt.IsSuccess() {
   312  		return rcpt, rcpt.Error()
   313  	}
   314  	return rcpt, nil
   315  }
   316  
   317  func (c *Context) SubscribeBlocks(cb rpc.ObserverCallback) (int, error) {
   318  	c.client.Listen()
   319  	id := c.client.BlockObserver.Subscribe(mavryk.ZeroOpHash, cb)
   320  	return id, nil
   321  }
   322  
   323  func (c *Context) UnsubscribeBlocks(id int) error {
   324  	c.client.BlockObserver.Unsubscribe(id)
   325  	return nil
   326  }
   327  
   328  func (c *Context) Params() *mavryk.Params {
   329  	return c.client.Params
   330  }
   331  
   332  func (c *Context) HeadBlock() *rpc.BlockHeaderLogEntry {
   333  	return c.client.BlockObserver.Head()
   334  }
   335  
   336  func (c *Context) SwitchLogger(tag, lvl string) {
   337  	if c.savedLoggers[0] == nil {
   338  		c.savedLoggers[0] = c.Log
   339  		c.savedLoggers[1] = c.client.Log
   340  	}
   341  	c.Log = c.savedLoggers[0].Clone().WithTag(tag).SetLevelString(lvl)
   342  	c.client.Log = c.savedLoggers[1].Clone().WithTag(tag).SetLevelString(lvl)
   343  }
   344  
   345  func (c *Context) RestoreLogger() {
   346  	if c.savedLoggers[0] != nil {
   347  		c.Log = c.savedLoggers[0]
   348  		c.client.Log = c.savedLoggers[1]
   349  	}
   350  	c.savedLoggers[0] = nil
   351  	c.savedLoggers[1] = nil
   352  }
   353  
   354  func (c *Context) WaitNumBlocks(n int) error {
   355  	if n == 0 {
   356  		return nil
   357  	}
   358  	done := make(chan struct{})
   359  	_, err := c.SubscribeBlocks(func(_ *rpc.BlockHeaderLogEntry, _ int64, _ int, _ int, _ bool) bool {
   360  		n--
   361  		if n <= 0 {
   362  			close(done)
   363  			return true
   364  		}
   365  		return false
   366  	})
   367  	if err != nil {
   368  		return err
   369  	}
   370  	select {
   371  	case <-done:
   372  		return nil
   373  	case <-c.Done():
   374  		return c.Err()
   375  	}
   376  }
   377  
   378  func (c Context) Fetch(u string, v any) error {
   379  	if !strings.HasPrefix(u, "http") {
   380  		u = c.url + u
   381  	}
   382  	raw, err := Fetch[json.RawMessage](c, u)
   383  	if err != nil {
   384  		return err
   385  	}
   386  	return json.Unmarshal(*raw, v)
   387  }