diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..64a897d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +name: Build + +on: [workflow_dispatch] + +jobs: + build_cli: + name: Build CLI + runs-on: ${{matrix.os}} + strategy: + matrix: + os: [ubuntu-20.04, windows-2022, macos-12] + arch: [amd64] + + steps: + - uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.19 + cache: true + + - name: Update Go modules + run: go mod download -json + + - name: Build CLI + run: go build -trimpath -o ./bin/cpm$(go env GOEXE) + env: + GOARCH: ${{ matrix.arch }} + + - name: Upload artifact + uses: actions/upload-artifact@v2 + with: + name: cpm-${{ matrix.os }}-${{ matrix.arch }} + path: ./bin/cpm* + if-no-files-found: error \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c507eb9 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# Installation +Download the tool from the releases page and place it somewhere on your path + +# Usage + +```shell +cpm -h +``` + +`cpm.json` is your project configuration file. Have a look or read more about it [here](docs/config.md). + +## Example commands + +### Download all contracts listed in `cpm.json` +Note that only `neo-express` is supported as destination chain. An [issue](https://github.com/nspcc-dev/neo-go/issues/2406) for `neo-go` to add support (go vote!). + +```shell +cpm --log-level DEBUG run +``` + +### Download a single contract or contract manifest +```shell +cpm download contract -c 0x4380f2c1de98bb267d3ea821897ec571a04fe3e0 -n mainnet +cpm download manifest -c 0x4380f2c1de98bb267d3ea821897ec571a04fe3e0 -N https://mainnet1.neo.coz.io:443 +``` + +### Build SDK from local manifest +```shell +cpm generate -m samplecontract.manifest.json -l python +cpm generate -m samplecontract.manifest.json -l go +``` +Note: the name from the manifest is used as output name diff --git a/config.go b/config.go new file mode 100644 index 0000000..bdbed09 --- /dev/null +++ b/config.go @@ -0,0 +1,118 @@ +package main + +import ( + _ "embed" + "encoding/json" + "fmt" + "github.com/nspcc-dev/neo-go/pkg/util" + log "github.com/sirupsen/logrus" + "io/ioutil" + "os" + "strings" +) + +//go:embed sampleconfig.json +var defaultConfig []byte +var cfg CPMConfig + +type ContractConfig struct { + Label string `json:"label"` + ScriptHash util.Uint160 `json:"script-hash"` + SourceNetwork *string `json:"source-network,omitempty"` + ContractGenerateSdk *bool `json:"contract-generate-sdk,omitempty"` +} + +type CPMConfig struct { + Defaults struct { + ContractSourceNetwork string `json:"contract-source-network"` + ContractDestination string `json:"contract-destination"` + ContractGenerateSdk bool `json:"contract-generate-sdk"` + SdkLanguage string `json:"sdk-language"` + } `json:"defaults"` + Contracts []ContractConfig `json:"contracts"` + Tools struct { + NeoExpress struct { + CanGenerateSDK bool `json:"canGenerateSDK"` + CanDownloadContract bool `json:"canDownloadContract"` + ExecutablePath *string `json:"executable-path"` + ConfigPath string `json:"config-path"` + } `json:"neo-express"` + } `json:"tools"` + Networks []struct { + Label string `json:"label"` + Hosts []string `json:"hosts"` + } `json:"networks"` +} + +func LoadConfig() { + f, err := os.Open(DEFAULT_CONFIG_FILE) + if err != nil { + if os.IsNotExist(err) { + log.Fatalf("Config file %s not found. Run `cpm init` to create a default config", DEFAULT_CONFIG_FILE) + } else { + log.Fatal(err) + } + } + defer f.Close() + + jsonData, _ := ioutil.ReadAll(f) + if err := json.Unmarshal(jsonData, &cfg); err != nil { + log.Fatal(fmt.Errorf("failed to parse config file: %w", err)) + } + + // ensure all contract configs can be worked with directly + for i, c := range cfg.Contracts { + if c.SourceNetwork == nil { + cfg.Contracts[i].SourceNetwork = &cfg.Defaults.ContractSourceNetwork + } + if c.ContractGenerateSdk == nil { + cfg.Contracts[i].ContractGenerateSdk = &cfg.Defaults.ContractGenerateSdk + } + } +} + +func CreateDefaultConfig() { + if _, err := os.Stat(DEFAULT_CONFIG_FILE); os.IsNotExist(err) { + err = ioutil.WriteFile(DEFAULT_CONFIG_FILE, defaultConfig, 0644) + if err != nil { + log.Fatal(err) + } + log.Infof("Written %s\n", DEFAULT_CONFIG_FILE) + } else { + log.Fatalf("%s already exists", DEFAULT_CONFIG_FILE) + } +} + +func (c *CPMConfig) getHosts(networkLabel string) []string { + for _, network := range c.Networks { + if network.Label == networkLabel { + return network.Hosts + } + } + log.Fatalf("Could not find hosts for label: %s", networkLabel) + return nil +} + +type EnumValue struct { + Enum []string + Default string + selected string +} + +func (e *EnumValue) Set(value string) error { + for _, enum := range e.Enum { + if enum == value { + e.selected = value + return nil + } + } + + return fmt.Errorf("allowed values are %s", strings.Join(e.Enum, ", ")) +} + +func (e EnumValue) String() string { + if e.selected == "" { + return e.Default + } + return e.selected +} diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 0000000..ec0fcb6 --- /dev/null +++ b/docs/config.md @@ -0,0 +1,49 @@ +`cpm.json` is your project configuration file. It holds all information about which contracts it should download, +from which network and whether it should generate an SDK that can quickly be consumed in the smart contract you're developing. + +It has 4 major sections which will be described in detail later on +* `defaults` - this section holds settings that apply to all contracts unless explicitly overridden in the `contracts` section. +* `contracts` - this section describes which contracts to download with what options. +* `tools` - this section describes the available tools and if they can be used for contract downloading and/or generating SDKs. +* `networks` - this section holds a list of networks with corresponding RPC server addresses to the networks used for source information downloading. + +# defaults +* `contract-source-network` - describes which network is the source for downloading contracts from. Valid values are [networks.label](#Networks)s. +* `contract-destination` - describe where the downloaded contract should be persisted. Valid values are [contract-destination](#contract-destination) keys. +* `contract-generate-sdk` - set to `true` to generate SDKs based on the contract manifest that can be consumed in your smart contract. +* `sdk-language` - the target language to generate the SDK in. Valid values: `python`. + +# contracts +* `label` - a user defined label to identify the target contract in the config. Must be a string. Not used elsewhere. +* `script-hash` - the script hash identifying the contract in `0x` format. i.e. `0x36d0bf624b90a9dad39d85dcafc83f14dab0272f`. +* `source-network` - (Optional) overrides the `contract-source-network` setting in `defaults` to set the source for downloading the contract from. Valid values are [networks.label](#Networks)s. +* `contract-generate-sdk` - (Optional) overrides the `contract-generate-sdk` setting in `defaults` to generate an SDK. Must be a bool value. + +# tools +Currently `neo-express` is the only tool that supports downloading contracts. An [issue](https://github.com/nspcc-dev/neo-go/issues/2406) exists for `neo-go` to add download support. +For SDK generation `python` is the only supported tool, but does not require a configuration section as it is part of the `cpm` package itself. Go-lang SDK generation exists but is still to be integrated. + +Each tool must specify the following 2 keys +* `canGenerateSDK` - indicates if the tool can be used for generating SDKs. Must be a bool value. +* `canDownloadContract` - indicates if the tool can be used for downloading contracts. Must be a bool value. + +Other keys are tool specific +* `neo-express` + * `express-path` - where to find the `neoxp` executable. Set to `null` if installed globally. Otherwise, specify the full path including the program name. + * `config-path` - where to find the `*.neo-express` configuration file of the target network. Must include the file name. i.e. `default.neo-express` if the file is in the root directory. + +Example + +```json +"neo-express": { + "canGenerateSDK": false, + "canDownloadContract": true, + "executable-path": null, + "config-path": "default.neo-express" +} +``` + + +# networks +* label - a user defined name for your network. Must be a string. +* hosts - a list of RPC addresses that all point to the same network. They will be queried in order until one of them gives a successful response. \ No newline at end of file diff --git a/downloader.go b/downloader.go new file mode 100644 index 0000000..8542cbf --- /dev/null +++ b/downloader.go @@ -0,0 +1,74 @@ +package main + +import ( + "bytes" + "fmt" + "github.com/nspcc-dev/neo-go/pkg/util" + log "github.com/sirupsen/logrus" + "os/exec" + "runtime" + "strings" +) + +type Downloader interface { + downloadContract(scriptHash util.Uint160, host string) (string, error) +} + +type NeoExpressDownloader struct { + expressConfigPath *string +} + +func NewNeoExpressDownloader(configPath string) Downloader { + executablePath := cfg.Tools.NeoExpress.ExecutablePath + if executablePath == nil { + var cmd *exec.Cmd + if runtime.GOOS == "darwin" { + cmd = exec.Command("bash", "-c", "neoxp -h") + } else { + cmd = exec.Command("neoxp", "-h") + } + err := cmd.Run() + if err != nil { + log.Fatal("Could not find 'neoxp' executable in $PATH. Please install neoxp globally using " + + "'dotnet tool install Neo.Express -g'" + + " or specify the 'executable-path' in cpm.json in the neo-express tools section") + } + } else { + // Verify path works by calling help (which has a 0 exit code) + cmd := exec.Command(*executablePath, "-h") + err := cmd.Run() + if err != nil { + log.Fatal(fmt.Errorf("could not find 'neoxp' executable in the configured executable-path: %w", err)) + } + } + return &NeoExpressDownloader{ + expressConfigPath: &configPath, + } +} + +func (ned *NeoExpressDownloader) downloadContract(scriptHash util.Uint160, host string) (string, error) { + // the name and arguments supplied to exec.Command differ slightly depending on the OS and whether neoxp is + // installed globally. the following are the base arguments that hold for all scenarios + args := []string{"contract", "download", "-i", cfg.Tools.NeoExpress.ConfigPath, "--force", "0x" + scriptHash.StringLE(), host} + + // global default + executable := "neoxp" + + if cfg.Tools.NeoExpress.ExecutablePath != nil { + executable = *cfg.Tools.NeoExpress.ExecutablePath + } else if runtime.GOOS == "darwin" { + executable = "bash" + tmp := append([]string{"neoxp"}, args...) + args = []string{"-c", strings.Join(tmp, " ")} + } + + cmd := exec.Command(executable, args...) + var errOut bytes.Buffer + cmd.Stderr = &errOut + out, err := cmd.Output() + if err != nil { + return "[NEOXP]" + errOut.String(), err + } else { + return "[NEOXP]" + string(out), nil + } +} diff --git a/generators/golang.go b/generators/golang.go new file mode 100644 index 0000000..301c994 --- /dev/null +++ b/generators/golang.go @@ -0,0 +1 @@ +package generators diff --git a/generators/python.go b/generators/python.go new file mode 100644 index 0000000..c9cf254 --- /dev/null +++ b/generators/python.go @@ -0,0 +1,226 @@ +package generators + +import ( + "fmt" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/util" + log "github.com/sirupsen/logrus" + "os" + "strconv" + "strings" + "text/template" +) + +/* + Big chunks of code gracefully borrowed from neo-go <3 with some adjustments + + Creates a Python SDK that can be easily used when writing smart contracts with neo3-boa. + The output is a Python package. For example for a contract named `samplecontract` it results in the folder structure + . + ├── samplecontract + │   ├── __init__.py + │   └── contract.py + + which can be used in your neo3-boa contract with + + from samplecontract import Samplecontract + Samplecontract.func1() +*/ + +type ( + PythonGenerateCfg struct { + Manifest *manifest.Manifest + ContractHash util.Uint160 + ContractOutput *os.File + } + + contractTmpl struct { + ContractName string + Imports []string + Hash string + Methods []methodTmpl + } + + methodTmpl struct { + Name string + NameABI string + Comment string + Arguments []paramTmpl + ReturnType string + } + + paramTmpl struct { + Name string + Type string + } +) + +const srcTmpl = ` +{{- define "METHOD" }} + @staticmethod + def {{.Name}}({{range $index, $arg := .Arguments -}} + {{- if ne $index 0}}, {{end}} + {{- .Name}}: {{.Type}} + {{- end}}) -> {{if .ReturnType }}{{ .ReturnType }}: {{ else }} None: {{ end }} + pass +{{- end -}} +from boa3.builtin.interop.contract import call_contract +from boa3.builtin.type import UInt160, UInt256, ECPoint +from boa3.builtin import contract +from typing import cast, Any + + +@contract('{{ .Hash }}') +class {{ .ContractName }}: +{{- range $m := .Methods}} +{{ template "METHOD" $m -}} +{{end}}` + +func GeneratePythonSDK(cfg *PythonGenerateCfg) error { + wd, err := os.Getwd() + + err = createSDKPackage(cfg) + defer cfg.ContractOutput.Close() + if err != nil { + return err + } + + wdContract, err := os.Getwd() + if err != nil { + return err + } + + ctr, err := templateFromManifest(cfg) + if err != nil { + return err + } + + tmp, err := template.New("generate").Parse(srcTmpl) + if err != nil { + return err + } + + err = tmp.Execute(cfg.ContractOutput, ctr) + if err != nil { + log.Fatal(err) + } + + log.Infof("Created SDK for contract '%s' at %s with contract hash 0x%s", cfg.Manifest.Name, wdContract, cfg.ContractHash.StringLE()) + + // change dir back to project root + os.Chdir(wd) + + return nil +} + +func templateFromManifest(cfg *PythonGenerateCfg) (contractTmpl, error) { + ctr := contractTmpl{ + ContractName: upperFirst(cfg.Manifest.Name), + Hash: "0x" + cfg.ContractHash.StringLE(), + } + + seen := make(map[string]bool) + for _, method := range cfg.Manifest.ABI.Methods { + seen[method.Name] = false + } + + for _, method := range cfg.Manifest.ABI.Methods { + if method.Name[0] == '_' { + continue + } + + name := method.Name + if v, ok := seen[name]; !ok || v { + suffix := strconv.Itoa(len(method.Parameters)) + for ; seen[name]; name = method.Name + suffix { + suffix = "_" + suffix + } + } + seen[name] = true + + mtd := methodTmpl{ + Name: name, + NameABI: method.Name, + Comment: fmt.Sprintf("invokes `%s` method of contract.", method.Name), + } + + for i := range method.Parameters { + name := method.Parameters[i].Name + if name == "" { + name = fmt.Sprintf("arg%d", i) + } + + var typeStr = scTypeToPython(method.Parameters[i].Type) + + mtd.Arguments = append(mtd.Arguments, paramTmpl{ + Name: name, + Type: typeStr, + }) + } + mtd.ReturnType = scTypeToPython(method.ReturnType) + ctr.Methods = append(ctr.Methods, mtd) + } + return ctr, nil +} + +// create the Python package structure and set the ContractOutput to the open file handle +func createSDKPackage(cfg *PythonGenerateCfg) error { + err := os.Mkdir(cfg.Manifest.Name, 0755) + if err != nil { + return fmt.Errorf("can't create directory %s: %w", cfg.Manifest.Name, err) + } + + _ = os.Chdir(cfg.Manifest.Name) + + f, err := os.Create("__init__.py") + if err != nil { + f.Close() + return fmt.Errorf("can't create __init__.py file: %w", err) + } else { + f.WriteString(fmt.Sprintf("from .contract import %s\n", upperFirst(cfg.Manifest.Name))) + f.Close() + } + + f, err = os.Create("contract.py") + if err != nil { + f.Close() + return fmt.Errorf("can't create contract.py file: %w", err) + } else { + cfg.ContractOutput = f + } + return nil +} + +func scTypeToPython(typ smartcontract.ParamType) string { + switch typ { + case smartcontract.AnyType, smartcontract.InteropInterfaceType: + return "Any" + case smartcontract.BoolType: + return "bool" + case smartcontract.IntegerType: + return "int" + case smartcontract.ByteArrayType: + return "bytes" + case smartcontract.StringType: + return "str" + case smartcontract.Hash160Type: + return "UInt160" + case smartcontract.Hash256Type: + return "UInt256" + case smartcontract.PublicKeyType: + return "ECPoint" + case smartcontract.ArrayType: + return "list" + case smartcontract.MapType: + return "dict" + case smartcontract.VoidType: + return "None" + default: + panic(fmt.Sprintf("unknown type: %T %s", typ, typ)) + } +} + +func upperFirst(s string) string { + return strings.ToUpper(s[0:1]) + s[1:] +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a908d87 --- /dev/null +++ b/go.mod @@ -0,0 +1,29 @@ +module cpm + +go 1.18 + +require ( + github.com/nspcc-dev/neo-go v0.99.2 + github.com/sirupsen/logrus v1.9.0 + github.com/urfave/cli/v2 v2.11.1 +) + +require ( + github.com/btcsuite/btcd v0.22.0-beta // indirect + github.com/coreos/go-semver v0.3.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/google/uuid v1.2.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 // indirect + github.com/nspcc-dev/rfc6979 v0.2.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + go.uber.org/atomic v1.9.0 // indirect + golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect + golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + golang.org/x/text v0.3.7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..64f37d6 --- /dev/null +++ b/go.sum @@ -0,0 +1,99 @@ +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta h1:LTDpDKUM5EeOFBPM8IXpinEcmZ6FWfNZbE3lfrfdnWo= +github.com/btcsuite/btcd v0.22.0-beta/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 h1:n4ZaFCKt1pQJd7PXoMJabZWK9ejjbLOVrkl/lOUmshg= +github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22/go.mod h1:79bEUDEviBHJMFV6Iq6in57FEOCMcRhfQnfaf0ETA5U= +github.com/nspcc-dev/neo-go v0.99.2 h1:Fq79FI6BJkj/XkgWtrURSdXgXIeBHCgbKauBw3LOvZ4= +github.com/nspcc-dev/neo-go v0.99.2/go.mod h1:9P0yWqhZX7i/ChJ+zjtiStO1uPTolPFUM+L5oNznU8E= +github.com/nspcc-dev/rfc6979 v0.2.0 h1:3e1WNxrN60/6N0DW7+UYisLeZJyfqZTNOjeV/toYvOE= +github.com/nspcc-dev/rfc6979 v0.2.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 h1:xQdMZ1WLrgkkvOZ/LDQxjVxMLdby7osSh4ZEVa5sIjs= +github.com/urfave/cli/v2 v2.11.1 h1:UKK6SP7fV3eKOefbS87iT9YHefv7iB/53ih6e+GNAsE= +github.com/urfave/cli/v2 v2.11.1/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= +github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo= +github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..af5e635 --- /dev/null +++ b/main.go @@ -0,0 +1,387 @@ +package main + +import ( + "context" + "cpm/generators" + _ "embed" + "encoding/json" + "fmt" + "github.com/nspcc-dev/neo-go/pkg/rpcclient" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/util" + log "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + "os" + "strings" +) + +var ( + TOOL_NEO_GO = "neo-go" + TOOL_NEO_EXPRESS = "neo-express" + + LANG_GO = "go" + LANG_PYTHON = "python" + + LOG_INFO = "INFO" + LOG_DEBUG = "DEBUG" + + DEFAULT_CONFIG_FILE = "cpm.json" +) + +func main() { + log.SetOutput(os.Stdout) + + app := &cli.App{ + Usage: "Contract Package Manager", + Flags: []cli.Flag{ + &cli.GenericFlag{ + Name: "log-level", + Usage: "Log output level", + Required: false, + Value: &EnumValue{ + Enum: []string{LOG_INFO, LOG_DEBUG}, + }, + }, + }, + Before: beforeAction, + Action: func(cCtx *cli.Context) error { + if cCtx.NArg() == 0 { + cli.ShowAppHelpAndExit(cCtx, 0) + } + return nil + }, + Commands: []*cli.Command{ + { + Name: "init", + Usage: "Create a new cpm.json config file", + Action: handleCliInit, + }, + { + Name: "run", + Usage: "Download all contracts from cpm.json and generate SDKs where specified", + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "download-only", Usage: "Override config settings to only download contracts and storage", Required: false}, + &cli.BoolFlag{Name: "sdk-only", Usage: "Override config settings to only generate SDKs", Required: false}, + //&cli.GenericFlag{ + // Name: "d", + // Usage: "Destination toolchain", + // Required: false, + // Value: &EnumValue{ + // Enum: []string{TOOL_NEO_EXPRESS}, + // }, + //}, + }, + Action: handleCliRun, + }, + { + Name: "download", + Usage: "Download contract or manifest", + Subcommands: []*cli.Command{ + { + Name: "contract", + Usage: "Download a single contract", + Flags: []cli.Flag{ + &cli.StringFlag{Name: "c", Usage: "Contract script hash", Required: true}, + &cli.StringFlag{Name: "n", Usage: "Source network label. Searches cpm.json for the network by label to find the host", Required: false}, + &cli.StringFlag{Name: "N", Usage: "Source network host", Required: false}, + &cli.StringFlag{Name: "i", Usage: "neo express config file", Required: false, DefaultText: "default.neo-express"}, + }, + Action: handleCliDownloadContract, + }, + { + Name: "manifest", + Usage: "Download the contract manifest", + Flags: []cli.Flag{ + &cli.StringFlag{Name: "c", Usage: "Contract script hash", Required: true}, + &cli.StringFlag{Name: "n", Usage: "Source network label. Searches cpm.json for the network by label to find the host", Required: false}, + &cli.StringFlag{Name: "N", Usage: "Source network host", Required: false}, + }, + Action: handleCliDownloadManifest, + }, + }, + }, + { + Name: "generate", + Usage: "Generate SDK from manifest", + Action: handleCliGenerate, + Flags: []cli.Flag{ + &cli.StringFlag{Name: "m", Usage: "Path to contract manifest.json", Required: true}, + &cli.StringFlag{Name: "c", Usage: "Contract script hash if known", Required: false}, + &cli.GenericFlag{ + Name: "l", + Usage: "SDK output language", + Required: true, // TODO: figure out why this is not working + Value: &EnumValue{ + Enum: []string{LANG_GO, LANG_PYTHON}, + }, + }, + }, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} + +func beforeAction(cCtx *cli.Context) error { + if cCtx.String("log-level") == LOG_DEBUG { + log.SetLevel(log.DebugLevel) + } + return nil +} + +func handleCliInit(*cli.Context) error { + CreateDefaultConfig() + return nil +} + +func handleCliRun(cCtx *cli.Context) error { + sdkOnly := cCtx.Bool("sdk-only") + downloadOnly := cCtx.Bool("download-only") + downloadContracts := !sdkOnly + generateSDKs := !downloadOnly + + if sdkOnly && downloadOnly { + log.Fatal("sdk-only and download-only flags are mutually exclusive.") + } + + LoadConfig() + + var downloader Downloader + // for now we only support NeoExpress + downloader = NewNeoExpressDownloader(cfg.Tools.NeoExpress.ConfigPath) + + for _, c := range cfg.Contracts { + log.Infof("Processing contract '%s' (%s)", c.Label, c.ScriptHash.StringLE()) + + hosts := cfg.getHosts(*c.SourceNetwork) + + success := false + generateSuccess := false + skipGenerate := *c.ContractGenerateSdk == false + for _, host := range hosts { + if downloadContracts { + log.Debugf("Attempting to download contract '%s' (%s) using NEOXP from network %s", c.Label, c.ScriptHash.StringLE(), host) + message, err := downloader.downloadContract(c.ScriptHash, host) + if err != nil { + // just log the error we got from the downloader and try the next host + log.Debug(message) + } else { + log.Info(message) + success = true + + if generateSDKs && !skipGenerate { + err := fetchManifestAndGenerateSDK(&c, host) + if err != nil { + log.Debug(err) + } else { + generateSuccess = true + } + } + } + } + if sdkOnly && *c.ContractGenerateSdk { + err := fetchManifestAndGenerateSDK(&c, host) + if err != nil { + log.Fatal(err) + } + } + } + + if downloadContracts && !success { + log.Fatalf("Failed to download contract '%s' (%s). Use '--log-level DEBUG' for more information", c.Label, c.ScriptHash) + } + + if generateSDKs && !skipGenerate && !generateSuccess { + log.Fatalf("Failed to generate SDK for contract '%s' (%s). Use '--log-level DEBUG' for more information", c.Label, c.ScriptHash) + } + } + + return nil +} + +func handleCliDownloadContract(cCtx *cli.Context) error { + networkLabel := cCtx.String("n") + networkHost := cCtx.String("N") + + var ( + scriptHash util.Uint160 + downloader Downloader + ) + + if networkLabel != "" && networkHost != "" { + log.Fatal("-n and -N flags are mutually exclusive") + } + + scriptHash, err := util.Uint160DecodeStringLE(strings.TrimPrefix(cCtx.String("c"), "0x")) + if err != nil { + return err + } + + LoadConfig() + + var hosts []string + if len(networkLabel) > 0 { + hosts = cfg.getHosts(networkLabel) + } else if len(networkHost) > 0 { + // TODO: sanity check value + hosts = []string{networkHost} + } else { + log.Fatal("Must specify either -n or -N flag") + } + + // for now, we only support NeoExpress + configPath := cfg.Tools.NeoExpress.ConfigPath + tmp := cCtx.String("i") + if len(tmp) > 0 { + configPath = tmp + } + downloader = NewNeoExpressDownloader(configPath) + + success := false + for _, host := range hosts { + message, err := downloader.downloadContract(scriptHash, host) + if err != nil { + // just log the error we got from the downloader and try the next host + log.Debug(message) + } else { + log.Info(message) + success = true + break + } + } + + if !success { + log.Fatalf("Failed to download contract %s. Use '--log-level DEBUG' for more information", scriptHash) + } + return nil +} + +func handleCliDownloadManifest(cCtx *cli.Context) error { + networkLabel := cCtx.String("n") + networkHost := cCtx.String("N") + + if networkLabel != "" && networkHost != "" { + log.Fatal("-n and -N flags are mutually exclusive") + } + + var hosts []string + if len(networkLabel) > 0 { + LoadConfig() + hosts = cfg.getHosts(networkLabel) + } else if len(networkHost) > 0 { + // TODO: sanity check value + hosts = []string{networkHost} + } else { + log.Fatal("Must specify either -n or -N flag") + } + + scriptHash, err := util.Uint160DecodeStringLE(strings.TrimPrefix(cCtx.String("c"), "0x")) + if err != nil { + return err + } + + for _, host := range hosts { + m, err := fetchManifest(&scriptHash, host) + if err != nil { + continue + } else { + f, err := os.Create("contract.manifest.json") + if err != nil { + return err + } + + out, err := json.MarshalIndent(m, "", " ") + if err != nil { + return err + } + + _, err = f.Write(out) + if err != nil { + return err + } + log.Info("Written manifest to contract.manifest.json") + return nil + } + } + + log.Fatalf("Failed to fetch manifest. Use '--log-level DEBUG' for more information") + return err +} + +func handleCliGenerate(cCtx *cli.Context) error { + m, _, err := readManifest(cCtx.String("m")) + if err != nil { + log.Fatalf("can't read contract manifest: %s", err) + } + + contractHash, _ := util.Uint160DecodeStringLE(strings.TrimPrefix(cCtx.String("c"), "0x")) + + language := cCtx.String("l") + return generateSDK(m, contractHash, language) +} + +func fetchManifestAndGenerateSDK(c *ContractConfig, host string) error { + m, err := fetchManifest(&c.ScriptHash, host) + if err != nil { + return err + } + + err = generateSDK(m, c.ScriptHash, cfg.Defaults.SdkLanguage) + if err != nil { + return err + } + return nil +} + +func fetchManifest(scriptHash *util.Uint160, host string) (*manifest.Manifest, error) { + opts := rpcclient.Options{} + client, err := rpcclient.New(context.TODO(), host, opts) + err = client.Init() + if err != nil { + log.Debug("RPCClient init failed with %v", err) + return nil, err + } + state, err := client.GetContractStateByHash(*scriptHash) + if err != nil { + log.Debug("get contractstate failed with %v", err) + return nil, err + } + return &state.Manifest, nil +} + +func readManifest(filename string) (*manifest.Manifest, []byte, error) { + if len(filename) == 0 { + return nil, nil, fmt.Errorf("no manifest file was found, specify manifest file with '-m' flag") + } + + manifestBytes, err := os.ReadFile(filename) + if err != nil { + return nil, nil, err + } + + m := new(manifest.Manifest) + err = json.Unmarshal(manifestBytes, m) + if err != nil { + return nil, nil, err + } + return m, manifestBytes, nil +} + +func generateSDK(m *manifest.Manifest, scriptHash util.Uint160, language string) error { + if language == LANG_PYTHON { + cfg := generators.PythonGenerateCfg{ + Manifest: m, + ContractHash: scriptHash, + } + err := generators.GeneratePythonSDK(&cfg) + if err != nil { + return err + } + } else { + log.Fatalf("%s is unsupported", language) + } + return nil +}