From 178da7c6a94718389e7192d87df5ee42e7223bc3 Mon Sep 17 00:00:00 2001 From: Péter Szilágyi Date: Mon, 5 Sep 2016 19:07:57 +0300 Subject: mobile: initial wrappers for mobile support --- accounts/abi/bind/bind.go | 161 +++++++++++++++++++++++++++++++++++++++-- accounts/abi/bind/bind_test.go | 2 +- accounts/abi/bind/template.go | 115 ++++++++++++++++++++++++++++- 3 files changed, 266 insertions(+), 12 deletions(-) (limited to 'accounts/abi') diff --git a/accounts/abi/bind/bind.go b/accounts/abi/bind/bind.go index 24fe9f770..154c67b0e 100644 --- a/accounts/abi/bind/bind.go +++ b/accounts/abi/bind/bind.go @@ -32,11 +32,20 @@ import ( "golang.org/x/tools/imports" ) +// Lang is a target programming language selector to generate bindings for. +type Lang int + +const ( + LangGo Lang = iota + LangJava + LangObjC +) + // Bind generates a Go wrapper around a contract ABI. This wrapper isn't meant // to be used as is in client code, but rather as an intermediate struct which // enforces compile time type safety and naming convention opposed to having to // manually maintain hard coded strings that break on runtime. -func Bind(types []string, abis []string, bytecodes []string, pkg string) (string, error) { +func Bind(types []string, abis []string, bytecodes []string, pkg string, lang Lang) (string, error) { // Process each individual contract requested binding contracts := make(map[string]*tmplContract) @@ -62,7 +71,7 @@ func Bind(types []string, abis []string, bytecodes []string, pkg string) (string for _, original := range evmABI.Methods { // Normalize the method for capital cases and non-anonymous inputs/outputs normalized := original - normalized.Name = capitalise(original.Name) + normalized.Name = methodNormalizer[lang](original.Name) normalized.Inputs = make([]abi.Argument, len(original.Inputs)) copy(normalized.Inputs, original.Inputs) @@ -78,7 +87,7 @@ func Bind(types []string, abis []string, bytecodes []string, pkg string) (string normalized.Outputs[j].Name = capitalise(output.Name) } } - // Append the methos to the call or transact lists + // Append the methods to the call or transact lists if original.Const { calls[original.Name] = &tmplMethod{Original: original, Normalized: normalized, Structured: structured(original)} } else { @@ -87,7 +96,7 @@ func Bind(types []string, abis []string, bytecodes []string, pkg string) (string } contracts[types[i]] = &tmplContract{ Type: capitalise(types[i]), - InputABI: strippedABI, + InputABI: strings.Replace(strippedABI, "\"", "\\\"", -1), InputBin: strings.TrimSpace(bytecodes[i]), Constructor: evmABI.Constructor, Calls: calls, @@ -102,9 +111,12 @@ func Bind(types []string, abis []string, bytecodes []string, pkg string) (string buffer := new(bytes.Buffer) funcs := map[string]interface{}{ - "bindtype": bindType, + "bindtype": bindType[lang], + "namedtype": namedType[lang], + "capitalise": capitalise, + "decapitalise": decapitalise, } - tmpl := template.Must(template.New("").Funcs(funcs).Parse(tmplSource)) + tmpl := template.Must(template.New("").Funcs(funcs).Parse(tmplSource[lang])) if err := tmpl.Execute(buffer, data); err != nil { return "", err } @@ -116,10 +128,17 @@ func Bind(types []string, abis []string, bytecodes []string, pkg string) (string return string(code), nil } -// bindType converts a Solidity type to a Go one. Since there is no clear mapping +// bindType is a set of type binders that convert Solidity types to some supported +// programming language. +var bindType = map[Lang]func(kind abi.Type) string{ + LangGo: bindTypeGo, + LangJava: bindTypeJava, +} + +// bindTypeGo converts a Solidity type to a Go one. Since there is no clear mapping // from all Solidity types to Go ones (e.g. uint17), those that cannot be exactly // mapped will use an upscaled type (e.g. *big.Int). -func bindType(kind abi.Type) string { +func bindTypeGo(kind abi.Type) string { stringKind := kind.String() switch { @@ -160,11 +179,137 @@ func bindType(kind abi.Type) string { } } +// bindTypeJava converts a Solidity type to a Java one. Since there is no clear mapping +// from all Solidity types to Java ones (e.g. uint17), those that cannot be exactly +// mapped will use an upscaled type (e.g. BigDecimal). +func bindTypeJava(kind abi.Type) string { + stringKind := kind.String() + + switch { + case strings.HasPrefix(stringKind, "address"): + parts := regexp.MustCompile("address(\\[[0-9]*\\])?").FindStringSubmatch(stringKind) + if len(parts) != 2 { + return stringKind + } + if parts[1] == "" { + return fmt.Sprintf("Address") + } + return fmt.Sprintf("Addresses") + + case strings.HasPrefix(stringKind, "bytes"): + parts := regexp.MustCompile("bytes([0-9]*)(\\[[0-9]*\\])?").FindStringSubmatch(stringKind) + if len(parts) != 3 { + return stringKind + } + if parts[2] != "" { + return "byte[][]" + } + return "byte[]" + + case strings.HasPrefix(stringKind, "int") || strings.HasPrefix(stringKind, "uint"): + parts := regexp.MustCompile("(u)?int([0-9]*)(\\[[0-9]*\\])?").FindStringSubmatch(stringKind) + if len(parts) != 4 { + return stringKind + } + switch parts[2] { + case "8", "16", "32", "64": + if parts[1] == "" { + if parts[3] == "" { + return fmt.Sprintf("int%s", parts[2]) + } + return fmt.Sprintf("int%s[]", parts[2]) + } + } + if parts[3] == "" { + return fmt.Sprintf("BigInt") + } + return fmt.Sprintf("BigInts") + + case strings.HasPrefix(stringKind, "bool"): + parts := regexp.MustCompile("bool(\\[[0-9]*\\])?").FindStringSubmatch(stringKind) + if len(parts) != 2 { + return stringKind + } + if parts[1] == "" { + return fmt.Sprintf("bool") + } + return fmt.Sprintf("bool[]") + + case strings.HasPrefix(stringKind, "string"): + parts := regexp.MustCompile("string(\\[[0-9]*\\])?").FindStringSubmatch(stringKind) + if len(parts) != 2 { + return stringKind + } + if parts[1] == "" { + return fmt.Sprintf("String") + } + return fmt.Sprintf("String[]") + + default: + return stringKind + } +} + +// namedType is a set of functions that transform language specific types to +// named versions that my be used inside method names. +var namedType = map[Lang]func(string, abi.Type) string{ + LangGo: func(string, abi.Type) string { panic("this shouldn't be needed") }, + LangJava: namedTypeJava, +} + +// namedTypeJava converts some primitive data types to named variants that can +// be used as parts of method names. +func namedTypeJava(javaKind string, solKind abi.Type) string { + switch javaKind { + case "byte[]": + return "Binary" + case "byte[][]": + return "Binaries" + case "string": + return "String" + case "string[]": + return "Strings" + case "bool": + return "Bool" + case "bool[]": + return "Bools" + case "BigInt": + parts := regexp.MustCompile("(u)?int([0-9]*)(\\[[0-9]*\\])?").FindStringSubmatch(solKind.String()) + if len(parts) != 4 { + return javaKind + } + switch parts[2] { + case "8", "16", "32", "64": + if parts[3] == "" { + return capitalise(fmt.Sprintf("%sint%s", parts[1], parts[2])) + } + return capitalise(fmt.Sprintf("%sint%ss", parts[1], parts[2])) + + default: + return javaKind + } + default: + return javaKind + } +} + +// methodNormalizer is a name transformer that modifies Solidity method names to +// conform to target language naming concentions. +var methodNormalizer = map[Lang]func(string) string{ + LangGo: capitalise, + LangJava: decapitalise, +} + // capitalise makes the first character of a string upper case. func capitalise(input string) string { return strings.ToUpper(input[:1]) + input[1:] } +// decapitalise makes the first character of a string lower case. +func decapitalise(input string) string { + return strings.ToLower(input[:1]) + input[1:] +} + // structured checks whether a method has enough information to return a proper // Go struct ot if flat returns are needed. func structured(method abi.Method) bool { diff --git a/accounts/abi/bind/bind_test.go b/accounts/abi/bind/bind_test.go index 67c09c3ad..6ebc8ea0a 100644 --- a/accounts/abi/bind/bind_test.go +++ b/accounts/abi/bind/bind_test.go @@ -398,7 +398,7 @@ func TestBindings(t *testing.T) { // Generate the test suite for all the contracts for i, tt := range bindTests { // Generate the binding and create a Go source file in the workspace - bind, err := Bind([]string{tt.name}, []string{tt.abi}, []string{tt.bytecode}, "bindtest") + bind, err := Bind([]string{tt.name}, []string{tt.abi}, []string{tt.bytecode}, "bindtest", LangGo) if err != nil { t.Fatalf("test %d: failed to generate binding: %v", i, err) } diff --git a/accounts/abi/bind/template.go b/accounts/abi/bind/template.go index 523122213..7225794a1 100644 --- a/accounts/abi/bind/template.go +++ b/accounts/abi/bind/template.go @@ -42,9 +42,16 @@ type tmplMethod struct { Structured bool // Whether the returns should be accumulated into a contract } -// tmplSource is the Go source template use to generate the contract binding +// tmplSource is language to template mapping containing all the supported +// programming languages the package can generate to. +var tmplSource = map[Lang]string{ + LangGo: tmplSourceGo, + LangJava: tmplSourceJava, +} + +// tmplSourceGo is the Go source template use to generate the contract binding // based on. -const tmplSource = ` +const tmplSourceGo = ` // This file is an automatically generated Go binding. Do not modify as any // change will likely be lost upon the next re-generation! @@ -52,7 +59,7 @@ package {{.Package}} {{range $contract := .Contracts}} // {{.Type}}ABI is the input ABI used to generate the binding from. - const {{.Type}}ABI = ` + "`" + `{{.InputABI}}` + "`" + ` + const {{.Type}}ABI = "{{.InputABI}}" {{if .InputBin}} // {{.Type}}Bin is the compiled bytecode used for deploying new contracts. @@ -258,3 +265,105 @@ package {{.Package}} {{end}} {{end}} ` + +// tmplSourceJava is the Java source template use to generate the contract binding +// based on. +const tmplSourceJava = ` +// This file is an automatically generated Java binding. Do not modify as any +// change will likely be lost upon the next re-generation! + +package {{.Package}}; + +import org.ethereum.geth.*; +import org.ethereum.geth.internal.*; + +{{range $contract := .Contracts}} + public class {{.Type}} { + // ABI is the input ABI used to generate the binding from. + public final static String ABI = "{{.InputABI}}"; + + {{if .InputBin}} + // Bytecode is the compiled bytecode used for deploying new contracts. + public final static byte[] Bytecode = "{{.InputBin}}".getBytes(); + + // deploy deploys a new Ethereum contract, binding an instance of {{.Type}} to it. + public static {{.Type}} deploy(TransactOpts auth, EthereumClient client{{range .Constructor.Inputs}}, {{bindtype .Type}} {{.Name}}{{end}}) throws Exception { + Interfaces args = Geth.newInterfaces({{(len .Constructor.Inputs)}}); + {{range $index, $element := .Constructor.Inputs}} + args.set({{$index}}, Geth.newInterface()); args.get({{$index}}).set{{namedtype (bindtype .Type) .Type}}({{.Name}}); + {{end}} + return new {{.Type}}(Geth.deployContract(auth, ABI, Bytecode, client, args)); + } + + // Internal constructor used by contract deployment. + private {{.Type}}(BoundContract deployment) { + this.Address = deployment.getAddress(); + this.Deployer = deployment.getDeployer(); + this.Contract = deployment; + } + {{end}} + + // Ethereum address where this contract is located at. + public final Address Address; + + // Ethereum transaction in which this contract was deployed (if known!). + public final Transaction Deployer; + + // Contract instance bound to a blockchain address. + private final BoundContract Contract; + + // Creates a new instance of {{.Type}}, bound to a specific deployed contract. + public {{.Type}}(Address address, EthereumClient client) throws Exception { + this(Geth.bindContract(address, ABI, client)); + } + + {{range .Calls}} + {{if gt (len .Normalized.Outputs) 1}} + // {{capitalise .Normalized.Name}}Results is the output of a call to {{.Normalized.Name}}. + public class {{capitalise .Normalized.Name}}Results { + {{range $index, $item := .Normalized.Outputs}}public {{bindtype .Type}} {{if ne .Name ""}}{{.Name}}{{else}}Return{{$index}}{{end}}; + {{end}} + } + {{end}} + + // {{.Normalized.Name}} is a free data retrieval call binding the contract method 0x{{printf "%x" .Original.Id}}. + // + // Solidity: {{.Original.String}} + public {{if gt (len .Normalized.Outputs) 1}}{{capitalise .Normalized.Name}}Results{{else}}{{range .Normalized.Outputs}}{{bindtype .Type}}{{end}}{{end}} {{.Normalized.Name}}(CallOpts opts{{range .Normalized.Inputs}}, {{bindtype .Type}} {{.Name}}{{end}}) throws Exception { + Interfaces args = Geth.newInterfaces({{(len .Normalized.Inputs)}}); + {{range $index, $item := .Normalized.Inputs}}args.set({{$index}}, Geth.newInterface()); args.get({{$index}}).set{{namedtype (bindtype .Type) .Type}}({{.Name}}); + {{end}} + + Interfaces results = Geth.newInterfaces({{(len .Normalized.Outputs)}}); + {{range $index, $item := .Normalized.Outputs}}Interface result{{$index}} = Geth.newInterface(); result{{$index}}.setDefault{{namedtype (bindtype .Type) .Type}}(); results.set({{$index}}, result{{$index}}); + {{end}} + + if (opts == null) { + opts = Geth.newCallOpts(); + } + this.Contract.call(opts, results, "{{.Original.Name}}", args); + {{if gt (len .Normalized.Outputs) 1}} + {{capitalise .Normalized.Name}}Results result = new {{capitalise .Normalized.Name}}Results(); + {{range $index, $item := .Normalized.Outputs}}result.{{if ne .Name ""}}{{.Name}}{{else}}Return{{$index}}{{end}} = results.get({{$index}}).get{{namedtype (bindtype .Type) .Type}}(); + {{end}} + return result; + {{else}}{{range .Normalized.Outputs}}return results.get(0).get{{namedtype (bindtype .Type) .Type}}();{{end}} + {{end}} + } + {{end}} + + {{range .Transacts}} + // {{.Normalized.Name}} is a paid mutator transaction binding the contract method 0x{{printf "%x" .Original.Id}}. + // + // Solidity: {{.Original.String}} + public Transaction {{.Normalized.Name}}(TransactOpts opts{{range .Normalized.Inputs}}, {{bindtype .Type}} {{.Name}}{{end}}) throws Exception { + Interfaces args = Geth.newInterfaces({{(len .Normalized.Inputs)}}); + {{range $index, $item := .Normalized.Inputs}}args.set({{$index}}, Geth.newInterface()); args.get({{$index}}).set{{namedtype (bindtype .Type) .Type}}({{.Name}}); + {{end}} + + return this.Contract.transact(opts, "{{.Original.Name}}" , args); + } + {{end}} + } +{{end}} +` -- cgit