github.com/mavryk-network/mvgo@v1.19.9/internal/compose/clone.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  	"encoding/hex"
     8  	"encoding/json"
     9  	"fmt"
    10  	"os"
    11  
    12  	"github.com/mavryk-network/mvgo/mavryk"
    13  	"github.com/mavryk-network/mvgo/micheline"
    14  )
    15  
    16  type CloneMode byte
    17  
    18  const (
    19  	CloneModeFile CloneMode = iota
    20  	CloneModeJson
    21  	CloneModeBinary
    22  	CloneModeUrl
    23  	CloneModeArgs
    24  )
    25  
    26  func (m CloneMode) String() string {
    27  	switch m {
    28  	case CloneModeFile:
    29  		return "file"
    30  	case CloneModeJson:
    31  		return "json"
    32  	case CloneModeBinary:
    33  		return "binary"
    34  	case CloneModeUrl:
    35  		return "url"
    36  	case CloneModeArgs:
    37  		return "args"
    38  	default:
    39  		return ""
    40  	}
    41  }
    42  
    43  func (m *CloneMode) Set(s string) (err error) {
    44  	switch s {
    45  	case "file":
    46  		*m = CloneModeFile
    47  	case "json":
    48  		*m = CloneModeJson
    49  	case "bin":
    50  		*m = CloneModeBinary
    51  	case "url":
    52  		*m = CloneModeUrl
    53  	case "args":
    54  		*m = CloneModeArgs
    55  	default:
    56  		err = fmt.Errorf("invalid clone mode")
    57  	}
    58  	return
    59  }
    60  
    61  type CloneConfig struct {
    62  	Name     string
    63  	Contract mavryk.Address
    64  	IndexUrl string
    65  	NumOps   uint
    66  	Path     string
    67  	Mode     CloneMode
    68  }
    69  
    70  type Op struct {
    71  	Type       string            `json:"type"`
    72  	Hash       string            `json:"hash"`
    73  	Height     int               `json:"height"`
    74  	OpP        int               `json:"op_p"`
    75  	OpC        int               `json:"op_c"`
    76  	OpI        int               `json:"op_i"`
    77  	IsInternal bool              `json:"is_internal"`
    78  	Sender     string            `json:"sender"`
    79  	Receiver   string            `json:"receiver"`
    80  	Amount     float64           `json:"volume"`
    81  	Script     *micheline.Script `json:"script"`
    82  	Params     *struct {
    83  		Entrypoint string         `json:"entrypoint"`
    84  		Prim       micheline.Prim `json:"prim"`
    85  	} `json:"parameters"`
    86  
    87  	// processed
    88  	Url           string `json:"-"`
    89  	PackedCode    string `json:"-"`
    90  	PackedStorage string `json:"-"`
    91  	PackedParams  string `json:"-"`
    92  	Args          any    `json:"-"`
    93  }
    94  
    95  func Clone(ctx Context, version string, cfg CloneConfig) error {
    96  	if !HasVersion(version) {
    97  		return ErrInvalidVersion
    98  	}
    99  	if !cfg.Contract.IsContract() {
   100  		return fmt.Errorf("invalid contract address")
   101  	}
   102  	if cfg.Name == "" {
   103  		cfg.Name = cfg.Contract.String()
   104  	}
   105  	ops, err := fetchOps(ctx, cfg)
   106  	if err != nil {
   107  		return err
   108  	}
   109  	eng := New(version)
   110  	buf, err := eng.Clone(ctx, ops, cfg)
   111  	if err != nil {
   112  		return err
   113  	}
   114  	err = os.WriteFile(cfg.Path, buf, 0644)
   115  	if err != nil {
   116  		return err
   117  	}
   118  	ctx.Log.Infof("File %s written successfully.", cfg.Path)
   119  	return nil
   120  }
   121  
   122  func fetchOps(ctx Context, cfg CloneConfig) ([]Op, error) {
   123  	ctx.Log.Infof("Fetching contract operations...")
   124  	u := fmt.Sprintf("%s/explorer/account/%s/operations?prim=1&storage=1&order=asc&limit=%d",
   125  		cfg.IndexUrl, cfg.Contract, cfg.NumOps+1)
   126  	resp, err := Fetch[[]Op](ctx, u)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  	ops := *resp
   131  	if len(ops) == 0 {
   132  		return nil, fmt.Errorf("contract %q has no transactions", cfg.Contract)
   133  	}
   134  	switch cfg.Mode {
   135  	case CloneModeFile:
   136  		err = storeOps(ctx, ops, cfg)
   137  	case CloneModeJson:
   138  		err = encodeJson(ops)
   139  	case CloneModeBinary:
   140  		err = encodeBinary(ops)
   141  	case CloneModeUrl:
   142  		encodeUrl(ops, ctx.url)
   143  	}
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  	if err := encodeArgs(ops); err != nil {
   148  		ctx.Log.Warnf("Marshaling args: %v", err)
   149  	}
   150  	return ops, nil
   151  }
   152  
   153  func storeOps(ctx Context, ops []Op, cfg CloneConfig) error {
   154  	for i := range ops {
   155  		var (
   156  			buf []byte
   157  			err error
   158  		)
   159  		ops[i].Url = generateFilename(cfg, ops[i], i)
   160  		switch ops[i].Type {
   161  		case "origination":
   162  			buf, err = json.Marshal(ops[i].Script)
   163  		case "transaction":
   164  			if ops[i].Params != nil {
   165  				buf, err = json.Marshal(ops[i].Params.Prim)
   166  			}
   167  		}
   168  		if err != nil {
   169  			return err
   170  		}
   171  		err = os.WriteFile(ops[i].Url, buf, 0644)
   172  		if err != nil {
   173  			return err
   174  		}
   175  		ctx.Log.Infof("File %s written.", ops[i].Url)
   176  	}
   177  	return nil
   178  }
   179  
   180  func encodeJson(ops []Op) error {
   181  	for i := range ops {
   182  		var (
   183  			buf []byte
   184  			err error
   185  		)
   186  		switch ops[i].Type {
   187  		case "origination":
   188  			buf, err = json.Marshal(ops[i].Script.Code)
   189  			if err == nil {
   190  				ops[i].PackedCode = string(buf)
   191  				buf, err = json.Marshal(ops[i].Script.Storage)
   192  			}
   193  			if err == nil {
   194  				ops[i].PackedStorage = string(buf)
   195  			}
   196  		case "transaction":
   197  			if ops[i].Params != nil {
   198  				buf, err = json.Marshal(ops[i].Params.Prim)
   199  				ops[i].PackedParams = string(buf)
   200  			}
   201  		}
   202  		if err != nil {
   203  			return err
   204  		}
   205  	}
   206  	return nil
   207  }
   208  
   209  func encodeBinary(ops []Op) error {
   210  	for i := range ops {
   211  		var (
   212  			buf []byte
   213  			err error
   214  		)
   215  		switch ops[i].Type {
   216  		case "origination":
   217  			buf, err = ops[i].Script.Code.MarshalBinary()
   218  			if err == nil {
   219  				ops[i].PackedCode = hex.EncodeToString(buf)
   220  				buf, err = ops[i].Script.Storage.MarshalBinary()
   221  			}
   222  			if err == nil {
   223  				ops[i].PackedStorage = hex.EncodeToString(buf)
   224  			}
   225  		case "transaction":
   226  			if ops[i].Params != nil {
   227  				buf, err = ops[i].Params.Prim.MarshalBinary()
   228  				ops[i].PackedParams = hex.EncodeToString(buf)
   229  			}
   230  		}
   231  		if err != nil {
   232  			return err
   233  		}
   234  	}
   235  	return nil
   236  }
   237  
   238  func encodeUrl(ops []Op, host string) {
   239  	for i, op := range ops {
   240  		switch op.Type {
   241  		case "origination":
   242  			if ops[i].IsInternal {
   243  				ops[i].Url = fmt.Sprintf(
   244  					"%s/chains/main/blocks/%d/operations/3/%d#contents.%d.metadata.internal_operation_results.%d.script",
   245  					host,
   246  					op.Height,
   247  					op.OpP,
   248  					op.OpC,
   249  					op.OpI,
   250  				)
   251  			} else {
   252  				ops[i].Url = fmt.Sprintf(
   253  					"%s/chains/main/blocks/%d/operations/3/%d#contents.%d.script",
   254  					host,
   255  					op.Height,
   256  					op.OpP,
   257  					op.OpC,
   258  				)
   259  			}
   260  		case "transaction":
   261  			if ops[i].IsInternal {
   262  				ops[i].Url = fmt.Sprintf(
   263  					"%s/chains/main/blocks/%d/operations/3/%d#contents.%d.metadata.internal_operation_results.%d.parameters.value",
   264  					host,
   265  					op.Height,
   266  					op.OpP,
   267  					op.OpC,
   268  					op.OpI,
   269  				)
   270  			} else {
   271  				ops[i].Url = fmt.Sprintf(
   272  					"%s/chains/main/blocks/%d/operations/3/%d#contents.%d.parameters.value",
   273  					host,
   274  					op.Height,
   275  					op.OpP,
   276  					op.OpC,
   277  				)
   278  			}
   279  		}
   280  	}
   281  }
   282  
   283  func encodeArgs(ops []Op) error {
   284  	var script *micheline.Script
   285  	for i, op := range ops {
   286  		switch op.Type {
   287  		case "origination":
   288  			script = op.Script
   289  			val := micheline.NewValue(script.StorageType(), script.Storage).
   290  				UnpackAllAsciiStrings()
   291  			res, err := val.Map()
   292  			if err != nil {
   293  				return err
   294  			}
   295  			ops[i].Args = res
   296  
   297  		case "transaction":
   298  			eps, err := script.Entrypoints(true)
   299  			if err != nil {
   300  				return err
   301  			}
   302  			ep, ok := eps[op.Params.Entrypoint]
   303  			if !ok {
   304  				return fmt.Errorf("missing entrypoint %s", op.Params.Entrypoint)
   305  			}
   306  			val := micheline.NewValue(ep.Type(), op.Params.Prim).
   307  				UnpackAllAsciiStrings()
   308  			res, err := val.Map()
   309  			if err != nil {
   310  				return err
   311  			}
   312  			ops[i].Args = res.(map[string]any)[op.Params.Entrypoint]
   313  		}
   314  	}
   315  	return nil
   316  }
   317  
   318  func generateFilename(cfg CloneConfig, op Op, index int) string {
   319  	return fmt.Sprintf("%02d-%s-%s.json", index, cfg.Name, op.Type)
   320  }