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 }