Skip to content

Commit

Permalink
smartcontract: support dynamic contract hash for bindings
Browse files Browse the repository at this point in the history
Allow dynamic contract hash for contract bindings.

Close #3007

Signed-off-by: Ekaterina Pavlova <ekt@morphbits.io>
  • Loading branch information
AliceInHunterland authored and AnnaShaleva committed Apr 18, 2024
1 parent f91bb1d commit 633157d
Show file tree
Hide file tree
Showing 5 changed files with 523 additions and 39 deletions.
104 changes: 82 additions & 22 deletions cli/smartcontract/contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
Expand Down Expand Up @@ -145,18 +146,7 @@ func Blocks() []*alias.Block {
cmd = append(cmd, "--in", ctrPath, "--bindings", bindingsPath)

// Replace `pkg/interop` in go.mod to avoid getting an actual module version.
goMod := filepath.Join(ctrPath, "go.mod")
data, err := os.ReadFile(goMod)
require.NoError(t, err)

i := bytes.IndexByte(data, '\n')
data = append([]byte("module myimport.com/testcontract"), data[i:]...)

wd, err := os.Getwd()
require.NoError(t, err)
data = append(data, "\nreplace github.com/nspcc-dev/neo-go/pkg/interop => "...)
data = append(data, filepath.Join(wd, "../../pkg/interop")...)
require.NoError(t, os.WriteFile(goMod, data, os.ModePerm))
require.NoError(t, updateGoMod(ctrPath, "myimport.com/testcontract", "../../pkg/interop"))

cmd = append(cmd, "--config", cfgPath,
"--out", filepath.Join(tmpDir, "out.nef"),
Expand All @@ -176,7 +166,7 @@ func Blocks() []*alias.Block {

bs, err := os.ReadFile(outPath)
require.NoError(t, err)
require.Equal(t, `// Code generated by neo-go contract generate-wrapper --manifest <file.json> --out <file.go> --hash <hash> [--config <config>]; DO NOT EDIT.
require.Equal(t, `// Code generated by neo-go contract generate-wrapper --manifest <file.json> --out <file.go> [--hash <hash>] [--config <config>]; DO NOT EDIT.
// Package testcontract contains wrappers for testcontract contract.
package testcontract
Expand Down Expand Up @@ -213,6 +203,84 @@ func ToMap(a []testcontract.MyPair) map[int]string {
`, string(bs))
}

// updateGoMod updates the go.mod file located in the specified directory.
// It sets the module name and replaces the neo-go interop package path with
// the provided one to avoid getting an actual module version.
func updateGoMod(dir, moduleName, neoGoPath string) error {
goModPath := filepath.Join(dir, "go.mod")
data, err := os.ReadFile(goModPath)
if err != nil {
return fmt.Errorf("failed to read go.mod: %w", err)
}

i := bytes.IndexByte(data, '\n')
if i == -1 {
return fmt.Errorf("unexpected go.mod format")
}

updatedData := append([]byte("module "+moduleName), data[i:]...)
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working directory: %w", err)
}

replacementPath := filepath.Join(wd, neoGoPath)
updatedData = append(updatedData, "\nreplace github.com/nspcc-dev/neo-go/pkg/interop => "+replacementPath+" \n"...)

if err := os.WriteFile(goModPath, updatedData, os.ModePerm); err != nil {
return fmt.Errorf("failed to write updated go.mod: %w", err)
}

return nil
}

func TestDynamicWrapper(t *testing.T) {
// For proper contract init. The actual version as it will be replaced.
smartcontract.ModVersion = "v0.0.0"

tmpDir := t.TempDir()
e := testcli.NewExecutor(t, true)

ctrPath := "../smartcontract/testdata"

verifyHash := testcli.DeployContract(t, e, filepath.Join(ctrPath, "verify.go"), filepath.Join(ctrPath, "verify.yml"), testcli.ValidatorWallet, testcli.ValidatorAddr, testcli.ValidatorPass)

helperContract := `package testcontract
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
verify "myimport.com/testcontract/bindings"
)
func CallVerifyContract(h interop.Hash160) bool{
contractInstance := verify.NewContract(h)
return contractInstance.Verify()
}`

helperDir := filepath.Join(tmpDir, "helper")
e.Run(t, "neo-go", "contract", "init", "--name", helperDir)

require.NoError(t, updateGoMod(helperDir, "myimport.com/testcontract", "../../pkg/interop"))
require.NoError(t, os.WriteFile(filepath.Join(helperDir, "main.go"), []byte(helperContract), os.ModePerm))
require.NoError(t, os.Mkdir(filepath.Join(helperDir, "bindings"), os.ModePerm))

e.Run(t, "neo-go", "contract", "generate-wrapper",
"--config", filepath.Join(ctrPath, "verify.bindings.yml"), "--manifest", filepath.Join(ctrPath, "verify.manifest.json"),
"--out", filepath.Join(helperDir, "bindings", "testdata.go"))
e.Run(t, "neo-go", "contract", "compile", "--in", filepath.Join(helperDir, "main.go"), "--config", filepath.Join(helperDir, "neo-go.yml"))
helperHash := testcli.DeployContract(t, e, filepath.Join(helperDir, "main.go"), filepath.Join(helperDir, "neo-go.yml"), testcli.ValidatorWallet, testcli.ValidatorAddr, testcli.ValidatorPass)

e.In.WriteString("one\r")
e.Run(t, "neo-go", "contract", "invokefunction",
"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
"--wallet", testcli.ValidatorWallet, "--address", testcli.ValidatorAddr, "--force", "--await", helperHash.StringLE(), "callVerifyContract", verifyHash.StringLE())

tx, _ := e.CheckTxPersisted(t, "Sent invocation transaction ")
aer, err := e.Chain.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
require.Equal(t, aer[0].Stack[0].Value().(bool), true)
}

func TestContractInitAndCompile(t *testing.T) {
// For proper contract init. The actual version as it will be replaced.
smartcontract.ModVersion = "v0.0.0"
Expand Down Expand Up @@ -265,15 +333,7 @@ func TestContractInitAndCompile(t *testing.T) {
})

// Replace `pkg/interop` in go.mod to avoid getting an actual module version.
goMod := filepath.Join(ctrPath, "go.mod")
data, err := os.ReadFile(goMod)
require.NoError(t, err)

wd, err := os.Getwd()
require.NoError(t, err)
data = append(data, "\nreplace github.com/nspcc-dev/neo-go/pkg/interop => "...)
data = append(data, filepath.Join(wd, "../../pkg/interop")...)
require.NoError(t, os.WriteFile(goMod, data, os.ModePerm))
require.NoError(t, updateGoMod(ctrPath, "myimport.com/testcontract", "../../pkg/interop"))

cmd = append(cmd, "--config", cfgPath)

Expand Down
27 changes: 15 additions & 12 deletions cli/smartcontract/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,22 @@ var generatorFlags = []cli.Flag{
},
cli.StringFlag{
Name: "hash",
Usage: "Smart-contract hash",
Usage: "Smart-contract hash. If not passed, the wrapper will be designed for dynamic hash usage",
},
}

var generateWrapperCmd = cli.Command{
Name: "generate-wrapper",
Usage: "generate wrapper to use in other contracts",
UsageText: "neo-go contract generate-wrapper --manifest <file.json> --out <file.go> --hash <hash> [--config <config>]",
Description: ``,
Action: contractGenerateWrapper,
Flags: generatorFlags,
Name: "generate-wrapper",
Usage: "generate wrapper to use in other contracts",
UsageText: "neo-go contract generate-wrapper --manifest <file.json> --out <file.go> [--hash <hash>] [--config <config>]",
Description: `Generates a Go wrapper to use it in other smart contracts. If the
--hash flag is provided, CALLT instruction is used for the target contract
invocation as an optimization of the wrapper contract code. If omitted, the
generated wrapper will be designed for dynamic hash usage, allowing
the hash to be specified at runtime.
`,
Action: contractGenerateWrapper,
Flags: generatorFlags,
}

var generateRPCWrapperCmd = cli.Command{
Expand All @@ -52,15 +57,15 @@ var generateRPCWrapperCmd = cli.Command{
}

func contractGenerateWrapper(ctx *cli.Context) error {
return contractGenerateSomething(ctx, binding.Generate, false)
return contractGenerateSomething(ctx, binding.Generate)
}

func contractGenerateRPCWrapper(ctx *cli.Context) error {
return contractGenerateSomething(ctx, rpcbinding.Generate, true)
return contractGenerateSomething(ctx, rpcbinding.Generate)
}

// contractGenerateSomething reads generator parameters and calls the given callback.
func contractGenerateSomething(ctx *cli.Context, cb func(binding.Config) error, allowEmptyHash bool) error {
func contractGenerateSomething(ctx *cli.Context, cb func(binding.Config) error) error {
if err := cmdargs.EnsureNone(ctx); err != nil {
return err
}
Expand All @@ -73,8 +78,6 @@ func contractGenerateSomething(ctx *cli.Context, cb func(binding.Config) error,
if err != nil {
return cli.NewExitError(fmt.Errorf("invalid contract hash: %w", err), 1)
}
} else if !allowEmptyHash {
return cli.NewExitError("contract hash must be provided via --hash flag", 1)
}
m, _, err := readManifest(ctx.String("manifest"), h)
if err != nil {
Expand Down
97 changes: 95 additions & 2 deletions cli/smartcontract/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ callflags:
"--hash", h.StringLE(),
}))

const expected = `// Code generated by neo-go contract generate-wrapper --manifest <file.json> --out <file.go> --hash <hash> [--config <config>]; DO NOT EDIT.
const expected = `// Code generated by neo-go contract generate-wrapper --manifest <file.json> --out <file.go> [--hash <hash>] [--config <config>]; DO NOT EDIT.
// Package wrapper contains wrappers for MyContract contract.
package wrapper
Expand Down Expand Up @@ -233,6 +233,99 @@ func MyFunc(in map[int]mycontract.Input) []mycontract.Output {
data, err := os.ReadFile(outFile)
require.NoError(t, err)
require.Equal(t, expected, string(data))

require.NoError(t, app.Run([]string{"", "generate-wrapper",
"--manifest", manifestFile,
"--config", cfgPath,
"--out", outFile,
}))
expectedWithDynamicHash := `// Code generated by neo-go contract generate-wrapper --manifest <file.json> --out <file.go> [--hash <hash>] [--config <config>]; DO NOT EDIT.
// Package wrapper contains wrappers for MyContract contract.
package wrapper
import (
"github.com/heyitsme/mycontract"
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/interop/iterator"
"github.com/nspcc-dev/neo-go/pkg/interop/native/ledger"
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
)
// Contract represents the MyContract smart contract.
type Contract struct {
Hash interop.Hash160
}
// NewContract returns a new Contract instance with the specified hash.
func NewContract(hash interop.Hash160) Contract {
return Contract{Hash: hash}
}
// Sum invokes ` + "`sum`" + ` method of contract.
func (c Contract) Sum(first int, second int) int {
return contract.Call(c.Hash, "sum", contract.All, first, second).(int)
}
// Sum2 invokes ` + "`sum`" + ` method of contract.
func (c Contract) Sum2(first int, second int, third int) int {
return contract.Call(c.Hash, "sum", contract.All, first, second, third).(int)
}
// Sum3 invokes ` + "`sum3`" + ` method of contract.
func (c Contract) Sum3() int {
return contract.Call(c.Hash, "sum3", contract.ReadOnly).(int)
}
// Zum invokes ` + "`zum`" + ` method of contract.
func (c Contract) Zum(typev int, typev_ int, funcv int) int {
return contract.Call(c.Hash, "zum", contract.All, typev, typev_, funcv).(int)
}
// JustExecute invokes ` + "`justExecute`" + ` method of contract.
func (c Contract) JustExecute(arr []any) {
contract.Call(c.Hash, "justExecute", contract.All, arr)
}
// GetPublicKey invokes ` + "`getPublicKey`" + ` method of contract.
func (c Contract) GetPublicKey() interop.PublicKey {
return contract.Call(c.Hash, "getPublicKey", contract.All).(interop.PublicKey)
}
// OtherTypes invokes ` + "`otherTypes`" + ` method of contract.
func (c Contract) OtherTypes(ctr interop.Hash160, tx interop.Hash256, sig interop.Signature, data any) bool {
return contract.Call(c.Hash, "otherTypes", contract.All, ctr, tx, sig, data).(bool)
}
// SearchStorage invokes ` + "`searchStorage`" + ` method of contract.
func (c Contract) SearchStorage(ctx storage.Context) iterator.Iterator {
return contract.Call(c.Hash, "searchStorage", contract.All, ctx).(iterator.Iterator)
}
// GetFromMap invokes ` + "`getFromMap`" + ` method of contract.
func (c Contract) GetFromMap(intMap map[string]int, indices []string) []int {
return contract.Call(c.Hash, "getFromMap", contract.All, intMap, indices).([]int)
}
// DoSomething invokes ` + "`doSomething`" + ` method of contract.
func (c Contract) DoSomething(bytes []byte, str string) any {
return contract.Call(c.Hash, "doSomething", contract.ReadStates, bytes, str).(any)
}
// GetBlockWrapper invokes ` + "`getBlockWrapper`" + ` method of contract.
func (c Contract) GetBlockWrapper() ledger.Block {
return contract.Call(c.Hash, "getBlockWrapper", contract.All).(ledger.Block)
}
// MyFunc invokes ` + "`myFunc`" + ` method of contract.
func (c Contract) MyFunc(in map[int]mycontract.Input) []mycontract.Output {
return contract.Call(c.Hash, "myFunc", contract.All, in).([]mycontract.Output)
}
`
data, err = os.ReadFile(outFile)
require.NoError(t, err)
require.Equal(t, expectedWithDynamicHash, string(data))
}

func TestGenerateValidPackageName(t *testing.T) {
Expand Down Expand Up @@ -267,7 +360,7 @@ func TestGenerateValidPackageName(t *testing.T) {

data, err := os.ReadFile(outFile)
require.NoError(t, err)
require.Equal(t, `// Code generated by neo-go contract generate-wrapper --manifest <file.json> --out <file.go> --hash <hash> [--config <config>]; DO NOT EDIT.
require.Equal(t, `// Code generated by neo-go contract generate-wrapper --manifest <file.json> --out <file.go> [--hash <hash>] [--config <config>]; DO NOT EDIT.
// Package myspacecontract contains wrappers for My space contract contract.
package myspacecontract
Expand Down
Loading

0 comments on commit 633157d

Please sign in to comment.