Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

generators: support off-chain Java SDK #27

Merged
merged 3 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ It has 4 major sections which will be described in detail later on
## GenerateConfig
* `languages` - a list of target languages to generate the SDK in.
* Valid values for `on-chain`: `csharp`, `go`, `java` and `python`.
* Valid values for `off-chain`: `ts` and `python`.
* Valid values for `off-chain`: `java`, `ts` and `python`.
* `destinations` - override default output path per language. Example
```yaml
on-chain:
Expand All @@ -46,7 +46,7 @@ It has 4 major sections which will be described in detail later on

# 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 on-chain SDK generation `C#`, `Java`, `Golang` and `Python` are supported. For off-chain SDK generation `ts` and `Python` are supported.
For on-chain SDK generation `C#`, `Java`, `Golang` and `Python` are supported. For off-chain SDK generation `Java`, `ts` and `Python` are supported.

Each tool must specify the following 2 keys
* `canGenerateSDK` - indicates if the tool can be used for generating SDKs. Must be a bool value.
Expand Down
24 changes: 14 additions & 10 deletions generators/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ const (

type (
GenerateCfg struct {
Manifest *manifest.Manifest
ContractHash util.Uint160
ContractOutput *os.File
ParamTypeConverter convertParam
MethodNameConverter func(s string) string
SdkDestination string
Manifest *manifest.Manifest
ContractHash util.Uint160
ContractOutput *os.File
ParamTypeConverter convertParam
MethodNameConverter func(s string) string
SdkDestination string
SupportMethodOverload bool
}

ContractTmpl struct {
Expand Down Expand Up @@ -80,12 +81,15 @@ func TemplateFromManifest(cfg *GenerateCfg) (ContractTmpl, error) {
}

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
if !cfg.SupportMethodOverload {
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{
Expand Down
278 changes: 278 additions & 0 deletions generators/java/offchain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
package java

import (
"cpm/generators"
"fmt"
"os"
"text/template"

"github.com/iancoleman/strcase"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
log "github.com/sirupsen/logrus"
)

const javaOffChainSrcTmpl = `
{{- define "INVOKEMETHOD" }}
public TransactionBuilder {{ .Name }}({{range $index, $arg := .Arguments -}}
{{- if ne $index 0}}, {{end}}{{.Type}} {{.Name}}
{{- end}}) {
return smartContract.invokeFunction("{{ .NameABI }}"{{if not .Arguments -}} ); {{- else}},
{{- $length := len .Arguments -}}
{{- range $index, $arg := .Arguments}}
{{ Neow3jWrapParameter $arg.TypeABI }}({{ .Name }}){{ if lt $index (Dec $length) }},{{ end }}
{{- end}}
);{{- end}}
}
{{- end -}}
{{- define "TESTINVOKEMETHOD" }}
public {{ Neow3jReturnType .ReturnType }} {{ if .Safe }}{{ .Name }}{{ else }}test{{ UpperFirst .Name }}{{ end }}({{range $index, $arg := .Arguments -}}
{{.Type}} {{.Name}}, {{ end }}AccountSigner... signers) {
{{- if ne .ReturnTypeABI "Void"}}
{{ if eq .ReturnTypeABI "InteropInterface" }}List<StackItem>{{ else }}NeoInvokeFunction{{ end }} response = null;
{{- end}}
try {
{{ $length := len .Arguments -}}
{{- if ne .ReturnTypeABI "Void"}}response = {{ end }}{{ if eq .ReturnTypeABI "InteropInterface" }}smartContract.callFunctionAndUnwrapIterator{{ else }}smartContract.callInvokeFunction{{ end }}(
"{{ .NameABI }}",
{{ if and (eq $length 0) (eq .ReturnTypeABI "InteropInterface") -}}
Collections.<ContractParameter>emptyList(),
{{ else if gt $length 0 -}}
{{- if eq $length 1 -}}
Collections.singletonList(
{{- else -}}
Arrays.asList(
{{- end -}}
{{- range $index, $arg := .Arguments }}
{{ Neow3jWrapParameter $arg.TypeABI }}({{ .Name }}){{ if lt $index (Dec $length) }},{{ end }}
{{- end }}
),
{{ end -}}
{{- if eq .ReturnTypeABI "InteropInterface" -}}
20,
{{ end -}}
signers
);
} catch (IOException e) {
throw new RuntimeException(e);
}
{{- if ne .ReturnTypeABI "Void" }}
return {{ Neow3jReturnTestInvoke .ReturnTypeABI }};
{{- end }}
}
{{- end -}}
package <REPLACE ME>

import io.neow3j.contract.SmartContract;
import io.neow3j.crypto.ECKeyPair;
import io.neow3j.protocol.Neow3j;
import io.neow3j.protocol.Neow3jConfig;
import io.neow3j.protocol.core.response.NeoInvokeFunction;
import io.neow3j.protocol.core.stackitem.StackItem;
import io.neow3j.protocol.http.HttpService;
import io.neow3j.transaction.AccountSigner;
import io.neow3j.transaction.TransactionBuilder;
import io.neow3j.types.ContractParameter;
import io.neow3j.types.Hash160;
import io.neow3j.types.Hash256;
import io.neow3j.utils.ArrayUtils;

import java.io.IOException;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

public class {{ .ContractName }} {
Neow3j neow3j;
Hash160 scriptHash;
SmartContract smartContract;

private void setScriptHash(Hash160 scriptHash) {
this.scriptHash = scriptHash;
}

private void setSmartContract(SmartContract smartContract) {
this.smartContract = smartContract;
}

public {{ .ContractName }}(String rpcAddress, Neow3jConfig neow3jConfig) {
neow3j = Neow3j.build(new HttpService(rpcAddress), neow3jConfig);
setScriptHash(new Hash160("{{ .Hash }}"));
setSmartContract(new SmartContract(scriptHash, neow3j));
}
{{ range $m := .Methods}}
{{- if .Safe }}
{{- template "TESTINVOKEMETHOD" $m -}}
{{- else }}
{{- template "INVOKEMETHOD" $m }}
{{ template "TESTINVOKEMETHOD" $m -}}
{{- end }}
{{end}}
}
`

func generateOffchainSDK(cfg *generators.GenerateCfg) error {
err := createJavaPackage(cfg)
defer cfg.ContractOutput.Close()
if err != nil {
return err
}

cfg.MethodNameConverter = strcase.ToLowerCamel
cfg.ParamTypeConverter = offchainScParameterTypeToJava
cfg.SupportMethodOverload = true
ctr, err := generators.TemplateFromManifest(cfg)
if err != nil {
return fmt.Errorf("failed to parse manifest into contract template: %v", err)
}

funcMap := template.FuncMap{
"Neow3jWrapParameter": neow3jWrapParameterTypes,
"Neow3jReturnType": changeListMapReturnTypeJava,
"Neow3jReturnTestInvoke": offchainJavaReturn,
"UpperFirst": generators.UpperFirst,
"Dec": decreaseNumber,
}

tmp, err := template.New("generate").Funcs(funcMap).Parse(javaOffChainSrcTmpl)
if err != nil {
return fmt.Errorf("failed to parse Java source template: %v", err)
}

err = tmp.Execute(cfg.ContractOutput, ctr)
if err != nil {
return fmt.Errorf("failed to generate Java code using template: %v", err)
}

wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working directory: %v", err)
}

log.Infof("Created SDK for contract '%s' at %s with contract hash 0x%s", cfg.Manifest.Name, wd+"/"+cfg.SdkDestination, cfg.ContractHash.StringLE())

return nil
}

func createOffchainJavaPackage(cfg *generators.GenerateCfg) error {
dir := cfg.SdkDestination
err := os.MkdirAll(dir, 0755)
if err != nil {
return fmt.Errorf("can't create directory %s: %w", dir, err)
}

filename := generators.UpperFirst(cfg.Manifest.Name)
f, err := os.Create(fmt.Sprintf(dir+"%s.java", filename))
if err != nil {
f.Close()
return fmt.Errorf("can't create %s.java file: %w", filename, err)
} else {
cfg.ContractOutput = f
}

return nil
}

func offchainScParameterTypeToJava(typ smartcontract.ParamType) string {
switch typ {
case smartcontract.AnyType:
return "Object"
case smartcontract.InteropInterfaceType:
return "List<?>"
case smartcontract.BoolType:
return "boolean"
case smartcontract.IntegerType:
return "BigInteger"
case smartcontract.ByteArrayType:
return "byte[]"
case smartcontract.StringType:
return "String"
case smartcontract.Hash160Type:
return "Hash160"
case smartcontract.Hash256Type:
return "Hash256"
case smartcontract.PublicKeyType:
return "ECKeyPair.ECPublicKey"
case smartcontract.ArrayType:
return "List<?>"
case smartcontract.MapType:
return "Map<?, ?>"
case smartcontract.VoidType:
return "void"
default:
panic(fmt.Sprintf("unknown type: %T %s", typ, typ))
}
}

func changeListMapReturnTypeJava(typ string) string {
if typ == "List<?>" {
return "List<StackItem>"
} else if typ == "Map<?, ?>" {
return "Map<StackItem, StackItem>"
} else {
return typ
}
}

func offchainJavaReturn(typ string) string {
switch typ {
case "Any":
return "response.getInvocationResult().getFirstStackItem().getValue()"
case "InteropInterface":
return "response"
case "Boolean":
return "response.getInvocationResult().getFirstStackItem().getBoolean()"
case "Integer":
return "response.getInvocationResult().getFirstStackItem().getInteger()"
case "ByteArray":
return "response.getInvocationResult().getFirstStackItem().getByteArray()"
case "String":
return "response.getInvocationResult().getFirstStackItem().getString()"
case "Hash160":
return "Hash160.fromAddress(response.getInvocationResult().getFirstStackItem().getAddress())"
case "Hash256":
return "new Hash256(ArrayUtils.reverseArray(response.getInvocationResult().getFirstStackItem().getByteArray()))"
case "PublicKey":
return "new ECKeyPair.ECPublicKey(response.getInvocationResult().getFirstStackItem().getHexString())"
case "Array":
return "response.getInvocationResult().getFirstStackItem().getList()"
case "Map":
return "response.getInvocationResult().getFirstStackItem().getMap()"
default:
panic(fmt.Sprintf("unknown type: %T %s", typ, typ))
}
}

func neow3jWrapParameterTypes(typ string) string {
switch typ {
case "Any":
return "ContractParameter.mapToContractParameter"
case "InteropInterface":
return "ContractParameter.any"
case "Boolean":
return "ContractParameter.bool"
case "Integer":
return "ContractParameter.integer"
case "ByteArray":
return "ContractParameter.byteArray"
case "String":
return "ContractParameter.string"
case "Hash160":
return "ContractParameter.hash160"
case "Hash256":
return "ContractParameter.hash256"
case "PublicKey":
return "ContractParameter.publicKey"
case "Array":
return "ContractParameter.array"
case "Map":
return "ContractParameter.map"
default:
panic(fmt.Sprintf("unknown type: %T %s", typ, typ))
}
}

func decreaseNumber(num int) int {
return num - 1
}
2 changes: 1 addition & 1 deletion generators/java/onchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class {{ .ContractName }} extends ContractInterface {
}
`

func GenerateJavaSDK(cfg *generators.GenerateCfg) error {
func generateOnchainSDK(cfg *generators.GenerateCfg) error {
err := createJavaPackage(cfg)
defer cfg.ContractOutput.Close()
if err != nil {
Expand Down
11 changes: 11 additions & 0 deletions generators/java/shared.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package java

import "cpm/generators"

func GenerateSDK(cfg *generators.GenerateCfg, sdkType string) error {
if sdkType == generators.SDKOnChain {
return generateOnchainSDK(cfg)
} else {
return generateOffchainSDK(cfg)
}
}
12 changes: 10 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,22 @@ func main() {
},
{
Name: LANG_JAVA,
Usage: "Generate an on-chain SDK for use with Java",
Usage: "Generate a SDK for use with Java",
Action: func(c *cli.Context) error {
return handleCliGenerate(c, LANG_JAVA)
},
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.StringFlag{Name: "o", Usage: "Output folder", Required: false},
&cli.GenericFlag{
Name: "t",
Usage: "SDK type",
Required: true,
Value: &EnumValue{
Enum: []string{generators.SDKOffChain, generators.SDKOnChain},
},
},
},
},
{
Expand Down Expand Up @@ -502,7 +510,7 @@ func generateSDK(cfg *generators.GenerateCfg, language, sdkType string) error {
if language == LANG_PYTHON {
err = python.GenerateSDK(cfg, sdkType)
} else if language == LANG_JAVA {
err = java.GenerateJavaSDK(cfg)
err = java.GenerateSDK(cfg, sdkType)
} else if language == LANG_CSHARP {
err = csharp.GenerateCsharpSDK(cfg)
} else if language == LANG_GO {
Expand Down