Skip to content

Commit

Permalink
start
Browse files Browse the repository at this point in the history
  • Loading branch information
ixje committed Aug 24, 2022
0 parents commit 7faf4d5
Show file tree
Hide file tree
Showing 10 changed files with 1,050 additions and 0 deletions.
35 changes: 35 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
118 changes: 118 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -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
}
49 changes: 49 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
@@ -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<hash>` 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.
74 changes: 74 additions & 0 deletions downloader.go
Original file line number Diff line number Diff line change
@@ -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
}
}
1 change: 1 addition & 0 deletions generators/golang.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package generators
Loading

0 comments on commit 7faf4d5

Please sign in to comment.