Commit 45c070fc authored by Jeffrey Wilcke's avatar Jeffrey Wilcke

Merge pull request #833 from ethersphere/frontier/solidity

solidity compiler and contract metadocs integration
parents 637b2415 009b2216
This diff is collapsed.
{"code":"605280600c6000396000f3006000357c010000000000000000000000000000000000000000000000000000000090048063c6888fa114602e57005b60376004356041565b8060005260206000f35b6000600782029050604d565b91905056","info":{"abiDefinition":[{"constant":false,"inputs":[{"name":"a","type":"uint256"}],"name":"multiply","outputs":[{"name":"d","type":"uint256"}],"type":"function"}],"compilerVersion":"0.9.13","developerDoc":{"methods":{}},"language":"Solidity","languageVersion":"0","source":"contract test {\n /// @notice Will multiply `a` by 7.\n function multiply(uint a) returns(uint d) {\n return a * 7;\n }\n}\n","userDoc":{"methods":{"multiply(uint256)":{"notice":"Will multiply `a` by 7."}}}}}
\ No newline at end of file
......@@ -20,6 +20,7 @@ package main
import (
"bufio"
"fmt"
"math/big"
"os"
"path"
"strings"
......@@ -62,19 +63,26 @@ type jsre struct {
re *re.JSRE
ethereum *eth.Ethereum
xeth *xeth.XEth
wait chan *big.Int
ps1 string
atexit func()
corsDomain string
prompter
}
func newJSRE(ethereum *eth.Ethereum, libPath string, interactive bool, corsDomain string) *jsre {
func newJSRE(ethereum *eth.Ethereum, libPath, solcPath, corsDomain string, interactive bool, f xeth.Frontend) *jsre {
js := &jsre{ethereum: ethereum, ps1: "> "}
// set default cors domain used by startRpc from CLI flag
js.corsDomain = corsDomain
js.xeth = xeth.New(ethereum, js)
if f == nil {
f = js
}
js.xeth = xeth.New(ethereum, f)
js.wait = js.xeth.UpdateState()
// update state in separare forever blocks
js.xeth.SetSolc(solcPath)
js.re = re.New(libPath)
js.apiBindings()
js.apiBindings(f)
js.adminBindings()
if !liner.TerminalSupported() || !interactive {
......@@ -87,18 +95,17 @@ func newJSRE(ethereum *eth.Ethereum, libPath string, interactive bool, corsDomai
js.atexit = func() {
js.withHistory(func(hist *os.File) { hist.Truncate(0); lr.WriteHistory(hist) })
lr.Close()
close(js.wait)
}
}
return js
}
func (js *jsre) apiBindings() {
ethApi := rpc.NewEthereumApi(js.xeth)
//js.re.Bind("jeth", rpc.NewJeth(ethApi, js.re.ToVal))
func (js *jsre) apiBindings(f xeth.Frontend) {
xe := xeth.New(js.ethereum, f)
ethApi := rpc.NewEthereumApi(xe)
jeth := rpc.NewJeth(ethApi, js.re.ToVal, js.re)
//js.re.Bind("jeth", jeth)
js.re.Set("jeth", struct{}{})
t, _ := js.re.Get("jeth")
jethObj := t.Object()
......@@ -143,13 +150,13 @@ var net = web3.net;
js.re.Eval(globalRegistrar + "registrar = new GlobalRegistrar(\"" + globalRegistrarAddr + "\");")
}
var ds, _ = docserver.New(utils.JSpathFlag.String())
var ds, _ = docserver.New("/")
func (self *jsre) ConfirmTransaction(tx string) bool {
if self.ethereum.NatSpec {
notice := natspec.GetNotice(self.xeth, tx, ds)
fmt.Println(notice)
answer, _ := self.Prompt("Confirm Transaction\n[y/n] ")
answer, _ := self.Prompt("Confirm Transaction [y/n]")
return strings.HasPrefix(strings.Trim(answer, " "), "y")
} else {
return true
......
This diff is collapsed.
......@@ -265,6 +265,7 @@ JavaScript API. See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Conso
utils.LogJSONFlag,
utils.PProfEanbledFlag,
utils.PProfPortFlag,
utils.SolcPathFlag,
}
app.Before = func(ctx *cli.Context) error {
if ctx.GlobalBool(utils.PProfEanbledFlag.Name) {
......@@ -320,7 +321,14 @@ func console(ctx *cli.Context) {
}
startEth(ctx, ethereum)
repl := newJSRE(ethereum, ctx.String(utils.JSpathFlag.Name), true, ctx.GlobalString(utils.RPCCORSDomainFlag.Name))
repl := newJSRE(
ethereum,
ctx.String(utils.JSpathFlag.Name),
ctx.String(utils.SolcPathFlag.Name),
ctx.GlobalString(utils.RPCCORSDomainFlag.Name),
true,
nil,
)
repl.interactive()
ethereum.Stop()
......@@ -335,7 +343,14 @@ func execJSFiles(ctx *cli.Context) {
}
startEth(ctx, ethereum)
repl := newJSRE(ethereum, ctx.String(utils.JSpathFlag.Name), false, ctx.GlobalString(utils.RPCCORSDomainFlag.Name))
repl := newJSRE(
ethereum,
ctx.String(utils.JSpathFlag.Name),
ctx.String(utils.SolcPathFlag.Name),
ctx.GlobalString(utils.RPCCORSDomainFlag.Name),
false,
nil,
)
for _, file := range ctx.Args() {
repl.exec(file)
}
......@@ -362,6 +377,7 @@ func unlockAccount(ctx *cli.Context, am *accounts.Manager, account string) (pass
func startEth(ctx *cli.Context, eth *eth.Ethereum) {
// Start Ethereum itself
utils.StartEthereum(eth)
am := eth.AccountManager()
......
......@@ -95,19 +95,10 @@ func initDataDir(Datadir string) {
}
}
func exit(err error) {
status := 0
if err != nil {
fmt.Fprintln(os.Stderr, "Fatal:", err)
status = 1
}
logger.Flush()
os.Exit(status)
}
// Fatalf formats a message to standard output and exits the program.
func Fatalf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, "Fatal: "+format+"\n", args...)
fmt.Fprintf(os.Stdout, "Fatal: "+format+"\n", args...)
logger.Flush()
os.Exit(1)
}
......@@ -115,7 +106,7 @@ func Fatalf(format string, args ...interface{}) {
func StartEthereum(ethereum *eth.Ethereum) {
glog.V(logger.Info).Infoln("Starting ", ethereum.Name())
if err := ethereum.Start(); err != nil {
exit(err)
Fatalf("Error starting Ethereum: %v", err)
}
RegisterInterrupt(func(sig os.Signal) {
ethereum.Stop()
......
......@@ -224,11 +224,17 @@ var (
Name: "shh",
Usage: "Enable whisper",
}
// ATM the url is left to the user and deployment to
JSpathFlag = cli.StringFlag{
Name: "jspath",
Usage: "JS library path to be used with console and js subcommands",
Value: ".",
}
SolcPathFlag = cli.StringFlag{
Name: "solc",
Usage: "solidity compiler to be used",
Value: "solc",
}
)
func GetNAT(ctx *cli.Context) nat.Interface {
......@@ -294,6 +300,7 @@ func MakeEthConfig(clientID, version string, ctx *cli.Context) *eth.Config {
Dial: true,
BootNodes: ctx.GlobalString(BootnodesFlag.Name),
}
}
func GetChain(ctx *cli.Context) (*core.ChainManager, common.Database, common.Database) {
......
package compiler
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger/glog"
)
const (
flair = "Christian <c@ethdev.com> and Lefteris <lefteris@ethdev.com> (c) 2014-2015"
languageVersion = "0"
)
var (
versionRegExp = regexp.MustCompile("[0-9]+.[0-9]+.[0-9]+")
params = []string{
"--binary", // Request to output the contract in binary (hexadecimal).
"file", //
"--json-abi", // Request to output the contract's JSON ABI interface.
"file", //
"--natspec-user", // Request to output the contract's Natspec user documentation.
"file", //
"--natspec-dev", // Request to output the contract's Natspec developer documentation.
"file",
}
)
type Contract struct {
Code string `json:"code"`
Info ContractInfo `json:"info"`
}
type ContractInfo struct {
Source string `json:"source"`
Language string `json:"language"`
LanguageVersion string `json:"languageVersion"`
CompilerVersion string `json:"compilerVersion"`
AbiDefinition interface{} `json:"abiDefinition"`
UserDoc interface{} `json:"userDoc"`
DeveloperDoc interface{} `json:"developerDoc"`
}
type Solidity struct {
solcPath string
version string
}
func New(solcPath string) (sol *Solidity, err error) {
// set default solc
if len(solcPath) == 0 {
solcPath = "solc"
}
solcPath, err = exec.LookPath(solcPath)
if err != nil {
return
}
cmd := exec.Command(solcPath, "--version")
var out bytes.Buffer
cmd.Stdout = &out
err = cmd.Run()
if err != nil {
return
}
version := versionRegExp.FindString(out.String())
sol = &Solidity{
solcPath: solcPath,
version: version,
}
glog.V(logger.Info).Infoln(sol.Info())
return
}
func (sol *Solidity) Info() string {
return fmt.Sprintf("solc v%s\nSolidity Compiler: %s\n%s", sol.version, sol.solcPath, flair)
}
func (sol *Solidity) Compile(source string) (contract *Contract, err error) {
if len(source) == 0 {
err = fmt.Errorf("empty source")
return
}
wd, err := ioutil.TempDir("", "solc")
if err != nil {
return
}
defer os.RemoveAll(wd)
in := strings.NewReader(source)
var out bytes.Buffer
// cwd set to temp dir
cmd := exec.Command(sol.solcPath, params...)
cmd.Dir = wd
cmd.Stdin = in
cmd.Stdout = &out
err = cmd.Run()
if err != nil {
err = fmt.Errorf("solc error: %v", err)
return
}
matches, _ := filepath.Glob(wd + "/*.binary")
if len(matches) < 1 {
err = fmt.Errorf("solc error: missing code output")
return
}
if len(matches) > 1 {
err = fmt.Errorf("multi-contract sources are not supported")
return
}
_, file := filepath.Split(matches[0])
base := strings.Split(file, ".")[0]
codeFile := path.Join(wd, base+".binary")
abiDefinitionFile := path.Join(wd, base+".abi")
userDocFile := path.Join(wd, base+".docuser")
developerDocFile := path.Join(wd, base+".docdev")
code, err := ioutil.ReadFile(codeFile)
if err != nil {
err = fmt.Errorf("error reading compiler output for code: %v", err)
return
}
abiDefinitionJson, err := ioutil.ReadFile(abiDefinitionFile)
if err != nil {
err = fmt.Errorf("error reading compiler output for abiDefinition: %v", err)
return
}
var abiDefinition interface{}
err = json.Unmarshal(abiDefinitionJson, &abiDefinition)
userDocJson, err := ioutil.ReadFile(userDocFile)
if err != nil {
err = fmt.Errorf("error reading compiler output for userDoc: %v", err)
return
}
var userDoc interface{}
err = json.Unmarshal(userDocJson, &userDoc)
developerDocJson, err := ioutil.ReadFile(developerDocFile)
if err != nil {
err = fmt.Errorf("error reading compiler output for developerDoc: %v", err)
return
}
var developerDoc interface{}
err = json.Unmarshal(developerDocJson, &developerDoc)
contract = &Contract{
Code: string(code),
Info: ContractInfo{
Source: source,
Language: "Solidity",
LanguageVersion: languageVersion,
CompilerVersion: sol.version,
AbiDefinition: abiDefinition,
UserDoc: userDoc,
DeveloperDoc: developerDoc,
},
}
return
}
func ExtractInfo(contract *Contract, filename string) (contenthash common.Hash, err error) {
contractInfo, err := json.Marshal(contract.Info)
if err != nil {
return
}
contenthash = common.BytesToHash(crypto.Sha3(contractInfo))
err = ioutil.WriteFile(filename, contractInfo, 0600)
return
}
package compiler
import (
"encoding/json"
"io/ioutil"
"os"
"testing"
"github.com/ethereum/go-ethereum/common"
)
var (
source = `
contract test {
/// @notice Will multiply ` + "`a`" + ` by 7.
function multiply(uint a) returns(uint d) {
return a * 7;
}
}
`
code = "605280600c6000396000f3006000357c010000000000000000000000000000000000000000000000000000000090048063c6888fa114602e57005b60376004356041565b8060005260206000f35b6000600782029050604d565b91905056"
info = `{"source":"\ncontract test {\n /// @notice Will multiply ` + "`a`" + ` by 7.\n function multiply(uint a) returns(uint d) {\n return a * 7;\n }\n}\n","language":"Solidity","languageVersion":"0","compilerVersion":"0.9.13","abiDefinition":[{"constant":false,"inputs":[{"name":"a","type":"uint256"}],"name":"multiply","outputs":[{"name":"d","type":"uint256"}],"type":"function"}],"userDoc":{"methods":{"multiply(uint256)":{"notice":"Will multiply ` + "`a`" + ` by 7."}}},"developerDoc":{"methods":{}}}`
infohash = common.HexToHash("0xfdb031637e8a1c1891143f8d129ebc7f7c4e4b41ecad8c85abe1756190f74204")
)
func TestCompiler(t *testing.T) {
sol, err := New("")
if err != nil {
t.Skip("no solc installed")
}
contract, err := sol.Compile(source)
if err != nil {
t.Errorf("error compiling source. result %v: %v", contract, err)
return
}
if contract.Code != code {
t.Errorf("wrong code, expected\n%s, got\n%s", code, contract.Code)
}
}
func TestCompileError(t *testing.T) {
sol, err := New("")
if err != nil {
t.Skip("no solc installed")
}
contract, err := sol.Compile(source[2:])
if err == nil {
t.Errorf("error expected compiling source. got none. result %v", contract)
return
}
}
func TestNoCompiler(t *testing.T) {
_, err := New("/path/to/solc")
if err != nil {
t.Log("solidity quits with error: %v", err)
} else {
t.Errorf("no solc installed, but got no error")
}
}
func TestExtractInfo(t *testing.T) {
var cinfo ContractInfo
err := json.Unmarshal([]byte(info), &cinfo)
if err != nil {
t.Errorf("%v", err)
}
contract := &Contract{
Code: "",
Info: cinfo,
}
filename := "/tmp/solctest.info.json"
os.Remove(filename)
cinfohash, err := ExtractInfo(contract, filename)
if err != nil {
t.Errorf("%v", err)
}
got, err := ioutil.ReadFile(filename)
if err != nil {
t.Errorf("%v", err)
}
if string(got) != info {
t.Errorf("incorrect info.json extracted, expected:\n%s\ngot\n%s", info, string(got))
}
if cinfohash != infohash {
t.Errorf("content hash for info is incorrect. expected %v, got %v", infohash.Hex(), cinfohash.Hex())
}
}
......@@ -17,118 +17,119 @@ import (
type abi2method map[[8]byte]*method
type NatSpec struct {
jsvm *otto.Otto
userDocJson, abiDocJson []byte
userDoc userDoc
tx, data string
// abiDoc abiDoc
}
func getFallbackNotice(comment, tx string) string {
return "About to submit transaction (" + comment + "): " + tx
jsvm *otto.Otto
abiDocJson []byte
userDoc userDoc
tx, data string
}
// main entry point for to get natspec notice for a transaction
// the implementation is frontend friendly in that it always gives back
// a notice that is safe to display
// :FIXME: the second return value is an error, which can be used to fine-tune bahaviour
func GetNotice(xeth *xeth.XEth, tx string, http *docserver.DocServer) (notice string) {
ns, err := New(xeth, tx, http)
if err != nil {
if ns == nil {
return getFallbackNotice("no NatSpec info found for contract", tx)
return getFallbackNotice(fmt.Sprintf("no NatSpec info found for contract: %v", err), tx)
} else {
return getFallbackNotice("invalid NatSpec info", tx)
return getFallbackNotice(fmt.Sprintf("invalid NatSpec info: %v", err), tx)
}
}
notice, err2 := ns.Notice()
if err2 != nil {
return getFallbackNotice("NatSpec notice error \""+err2.Error()+"\"", tx)
notice, err = ns.Notice()
if err != nil {
return getFallbackNotice(fmt.Sprintf("NatSpec notice error: %v", err), tx)
}
return
}
func getFallbackNotice(comment, tx string) string {
return fmt.Sprintf("About to submit transaction (%s): %s", comment, tx)
}
func New(xeth *xeth.XEth, tx string, http *docserver.DocServer) (self *NatSpec, err error) {
type transaction struct {
To string `json:"to"`
Data string `json:"data"`
}
// extract contract address from tx
type jsonTx struct {
Params []transaction `json:"params"`
}
type contractInfo struct {
Source string `json:"source"`
Language string `json:"language"`
Version string `json:"compilerVersion"`
AbiDefinition json.RawMessage `json:"abiDefinition"`
UserDoc userDoc `json:"userDoc"`
DeveloperDoc json.RawMessage `json:"developerDoc"`
}
var obj map[string]json.RawMessage
err = json.Unmarshal([]byte(tx), &obj)
func New(xeth *xeth.XEth, jsontx string, http *docserver.DocServer) (self *NatSpec, err error) {
// extract contract address from tx
var tx jsonTx
err = json.Unmarshal([]byte(jsontx), &tx)
if err != nil {
return
}
var tmp []map[string]string
err = json.Unmarshal(obj["params"], &tmp)
t := tx.Params[0]
contractAddress := t.To
content, err := FetchDocsForContract(contractAddress, xeth, http)
if err != nil {
return
}
contractAddress := tmp[0]["to"]
self, err = NewWithDocs(content, jsontx, t.Data)
return
}
// also called by admin.contractInfo.get
func FetchDocsForContract(contractAddress string, xeth *xeth.XEth, http *docserver.DocServer) (content []byte, err error) {
// retrieve contract hash from state
if !xeth.IsContract(contractAddress) {
err = fmt.Errorf("NatSpec error: contract not found")
return
}
codehex := xeth.CodeAt(contractAddress)
codeHash := common.BytesToHash(crypto.Sha3(common.Hex2Bytes(codehex[2:])))
// parse out host/domain
codeb := xeth.CodeAtBytes(contractAddress)
if codehex == "0x" {
err = fmt.Errorf("contract (%v) not found", contractAddress)
return
}
codehash := common.BytesToHash(crypto.Sha3(codeb))
// set up nameresolver with natspecreg + urlhint contract addresses
res := resolver.New(
xeth,
resolver.URLHintContractAddress,
resolver.HashRegContractAddress,
)
res := resolver.New(xeth)
// resolve host via HashReg/UrlHint Resolver
uri, hash, err := res.KeyToUrl(codeHash)
uri, hash, err := res.KeyToUrl(codehash)
if err != nil {
return
}
// get content via http client and authenticate content using hash
content, err := http.GetAuthContent(uri, hash)
if err != nil {
return
}
// get abi, userdoc
var obj2 map[string]json.RawMessage
err = json.Unmarshal(content, &obj2)
content, err = http.GetAuthContent(uri, hash)
if err != nil {
return
}
abi := []byte(obj2["abi"])
userdoc := []byte(obj2["userdoc"])
self, err = NewWithDocs(abi, userdoc, tx)
return
}
func NewWithDocs(abiDocJson, userDocJson []byte, tx string) (self *NatSpec, err error) {
func NewWithDocs(infoDoc []byte, tx string, data string) (self *NatSpec, err error) {
var obj map[string]json.RawMessage
err = json.Unmarshal([]byte(tx), &obj)
var contract contractInfo
err = json.Unmarshal(infoDoc, &contract)
if err != nil {
return
}
var tmp []map[string]string
err = json.Unmarshal(obj["params"], &tmp)
if err != nil {
return
}
data := tmp[0]["data"]
self = &NatSpec{
jsvm: otto.New(),
abiDocJson: abiDocJson,
userDocJson: userDocJson,
tx: tx,
data: data,
jsvm: otto.New(),
abiDocJson: []byte(contract.AbiDefinition),
userDoc: contract.UserDoc,
tx: tx,
data: data,
}
// load and require natspec js (but it is meant to be protected environment)
......@@ -137,13 +138,6 @@ func NewWithDocs(abiDocJson, userDocJson []byte, tx string) (self *NatSpec, err
return
}
_, err = self.jsvm.Run("var natspec = require('natspec');")
if err != nil {
return
}
err = json.Unmarshal(userDocJson, &self.userDoc)
// err = parseAbiJson(abiDocJson, &self.abiDoc)
return
}
......
This diff is collapsed.
......@@ -4,70 +4,65 @@ import (
"testing"
)
func makeUserdoc(desc string) []byte {
func makeInfoDoc(desc string) []byte {
return []byte(`
{
"source": "...",
"source": "contract test { }",
"language": "Solidity",
"languageVersion": 1,
"methods": {
"multiply(uint256)": {
"notice": "` + desc + `"
"compilerVersion": "1",
"userDoc": {
"methods": {
"multiply(uint256)": {
"notice": "` + desc + `"
},
"balance(address)": {
"notice": "` + "`(balanceInmGAV / 1000).fixed(0,3)`" + ` GAV is the total funds available to ` + "`who.address()`." + `"
}
},
"balance(address)": {
"notice": "` + "`(balanceInmGAV / 1000).fixed(0,3)`" + ` GAV is the total funds available to ` + "`who.address()`." + `"
}
"invariants": [
{ "notice": "The sum total amount of GAV in the system is 1 million." }
],
"construction": [
{ "notice": "Endows ` + "`message.caller.address()`" + ` with 1m GAV." }
]
},
"invariants": [
{ "notice": "The sum total amount of GAV in the system is 1 million." }
],
"construction": [
{ "notice": "Endows ` + "`message.caller.address()`" + ` with 1m GAV." }
]
}
`)
"abiDefinition": [{
"name": "multiply",
"constant": false,
"type": "function",
"inputs": [{
"name": "a",
"type": "uint256"
}],
"outputs": [{
"name": "d",
"type": "uint256"
}]
}]
}`)
}
var data = "0xc6888fa1000000000000000000000000000000000000000000000000000000000000007a"
var tx = `
{
"jsonrpc": "2.0",
"method": "eth_call",
"params": [{
"to": "0x8521742d3f456bd237e312d6e30724960f72517a",
"data": "0xc6888fa1000000000000000000000000000000000000000000000000000000000000007a"
}],
"id": 6
}
`
var abi = []byte(`
[{
"name": "multiply",
"constant": false,
"type": "function",
"inputs": [{
"name": "a",
"type": "uint256"
}],
"outputs": [{
"name": "d",
"type": "uint256"
}]
}]
`)
func TestNotice(t *testing.T) {
desc := "Will multiply `a` by 7 and return `a * 7`."
expected := "Will multiply 122 by 7 and return 854."
userdoc := makeUserdoc(desc)
ns, err := NewWithDocs(abi, userdoc, tx)
infodoc := makeInfoDoc(desc)
ns, err := NewWithDocs(infodoc, tx, data)
if err != nil {
t.Errorf("New: error: %v", err)
return
}
notice, err := ns.Notice()
......@@ -78,8 +73,6 @@ func TestNotice(t *testing.T) {
if notice != expected {
t.Errorf("incorrect notice. expected %v, got %v", expected, notice)
} else {
t.Logf("returned notice \"%v\"", notice)
}
}
......@@ -87,10 +80,10 @@ func TestNotice(t *testing.T) {
func TestMissingMethod(t *testing.T) {
desc := "Will multiply `a` by 7 and return `a * 7`."
userdoc := makeUserdoc(desc)
expected := "natspec.js error evaluating expression: Natspec evaluation failed, method does not exist"
ns, err := NewWithDocs(abi, userdoc, tx)
infodoc := makeInfoDoc(desc)
ns, err := NewWithDocs(infodoc, tx, data)
if err != nil {
t.Errorf("New: error: %v", err)
}
......@@ -113,9 +106,8 @@ func TestInvalidDesc(t *testing.T) {
desc := "Will multiply 122 by \"7\" and return 854."
expected := "invalid character '7' after object key:value pair"
userdoc := makeUserdoc(desc)
_, err := NewWithDocs(abi, userdoc, tx)
infodoc := makeInfoDoc(desc)
_, err := NewWithDocs(infodoc, tx, data)
if err == nil {
t.Errorf("expected error, got nothing", err)
} else {
......@@ -131,9 +123,8 @@ func TestWrongInputParams(t *testing.T) {
desc := "Will multiply `e` by 7 and return `a * 7`."
expected := "natspec.js error evaluating expression: Natspec evaluation failed, wrong input params"
userdoc := makeUserdoc(desc)
ns, err := NewWithDocs(abi, userdoc, tx)
infodoc := makeInfoDoc(desc)
ns, err := NewWithDocs(infodoc, tx, data)
if err != nil {
t.Errorf("New: error: %v", err)
}
......
......@@ -6,7 +6,8 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
xe "github.com/ethereum/go-ethereum/xeth"
"github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger/glog"
)
/*
......@@ -18,50 +19,152 @@ The resolver is meant to be called by the roundtripper transport implementation
of a url scheme
*/
// contract addresses will be hardcoded after they're created
var URLHintContractAddress string = "0000000000000000000000000000000000000000000000000000000000001234"
var HashRegContractAddress string = "0000000000000000000000000000000000000000000000000000000000005678"
// // contract addresses will be hardcoded after they're created
var UrlHintContractAddress, HashRegContractAddress string
func CreateContracts(xeth *xe.XEth, addr string) {
var err error
URLHintContractAddress, err = xeth.Transact(addr, "", "", "100000000000", "1000000", "100000", ContractCodeURLhint)
const (
txValue = "0"
txGas = "100000"
txGasPrice = "1000000000000"
)
func abi(s string) string {
return common.ToHex(crypto.Sha3([]byte(s))[:4])
}
var (
registerContentHashAbi = abi("register(uint256,uint256)")
registerUrlAbi = abi("register(uint256,uint8,uint256)")
setOwnerAbi = abi("setowner()")
)
type Backend interface {
StorageAt(string, string) string
Transact(fromStr, toStr, nonceStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, error)
}
type Resolver struct {
backend Backend
}
func New(eth Backend) *Resolver {
return &Resolver{eth}
}
// for testing and play temporarily
// ideally the HashReg and UrlHint contracts should be in the genesis block
// if we got build-in support for natspec/contract info
// there should be only one of these officially endorsed
// addresses as constants
// TODO: could get around this with namereg, check
func (self *Resolver) CreateContracts(addr common.Address) (err error) {
HashRegContractAddress, err = self.backend.Transact(addr.Hex(), "", "", txValue, txGas, txGasPrice, ContractCodeHashReg)
if err != nil {
panic(err)
return
}
HashRegContractAddress, err = xeth.Transact(addr, "", "", "100000000000", "1000000", "100000", ContractCodeHashReg)
UrlHintContractAddress, err = self.backend.Transact(addr.Hex(), "", "", txValue, txGas, txGasPrice, ContractCodeURLhint)
glog.V(logger.Detail).Infof("HashReg @ %v\nUrlHint @ %v\n", HashRegContractAddress, UrlHintContractAddress)
return
}
// called as first step in the registration process on HashReg
func (self *Resolver) SetOwner(address common.Address) (txh string, err error) {
return self.backend.Transact(
address.Hex(),
HashRegContractAddress,
"", txValue, txGas, txGasPrice,
setOwnerAbi,
)
}
// registers some content hash to a key/code hash
// e.g., the contract Info combined Json Doc's ContentHash
// to CodeHash of a contract or hash of a domain
// kept
func (self *Resolver) RegisterContentHash(address common.Address, codehash, dochash common.Hash) (txh string, err error) {
_, err = self.SetOwner(address)
if err != nil {
panic(err)
return
}
codehex := common.Bytes2Hex(codehash[:])
dochex := common.Bytes2Hex(dochash[:])
data := registerContentHashAbi + codehex + dochex
return self.backend.Transact(
address.Hex(),
HashRegContractAddress,
"", txValue, txGas, txGasPrice,
data,
)
}
type Resolver struct {
backend Backend
urlHintContractAddress string
hashRegContractAddress string
// registers a url to a content hash so that the content can be fetched
// address is used as sender for the transaction and will be the owner of a new
// registry entry on first time use
// FIXME: silently doing nothing if sender is not the owner
// note that with content addressed storage, this step is no longer necessary
// it could be purely
func (self *Resolver) RegisterUrl(address common.Address, hash common.Hash, url string) (txh string, err error) {
hashHex := common.Bytes2Hex(hash[:])
var urlHex string
urlb := []byte(url)
var cnt byte
n := len(urlb)
for n > 0 {
if n > 32 {
n = 32
}
urlHex = common.Bytes2Hex(urlb[:n])
urlb = urlb[n:]
n = len(urlb)
bcnt := make([]byte, 32)
bcnt[31] = cnt
data := registerUrlAbi +
hashHex +
common.Bytes2Hex(bcnt) +
common.Bytes2Hex(common.Hex2BytesFixed(urlHex, 32))
txh, err = self.backend.Transact(
address.Hex(),
UrlHintContractAddress,
"", txValue, txGas, txGasPrice,
data,
)
if err != nil {
return
}
cnt++
}
return
}
type Backend interface {
StorageAt(string, string) string
}
func (self *Resolver) Register(address common.Address, codehash, dochash common.Hash, url string) (txh string, err error) {
func New(eth Backend, uhca, nrca string) *Resolver {
return &Resolver{eth, uhca, nrca}
_, err = self.RegisterContentHash(address, codehash, dochash)
if err != nil {
return
}
return self.RegisterUrl(address, dochash, url)
}
// resolution is costless non-transactional
// implemented as direct retrieval from db
func (self *Resolver) KeyToContentHash(khash common.Hash) (chash common.Hash, err error) {
// look up in hashReg
at := common.Bytes2Hex(common.FromHex(HashRegContractAddress))
key := storageAddress(storageMapping(storageIdx2Addr(1), khash[:]))
hash := self.backend.StorageAt(self.hashRegContractAddress, key)
hash := self.backend.StorageAt(at, key)
if hash == "0x0" || len(hash) < 3 {
err = fmt.Errorf("GetHashReg: content hash not found")
err = fmt.Errorf("content hash not found for '%v'", khash.Hex())
return
}
copy(chash[:], common.Hex2BytesFixed(hash[2:], 32))
return
}
// retrieves the url-hint for the content hash -
// if we use content addressed storage, this step is no longer necessary
func (self *Resolver) ContentHashToUrl(chash common.Hash) (uri string, err error) {
// look up in URL reg
var str string = " "
......@@ -69,7 +172,7 @@ func (self *Resolver) ContentHashToUrl(chash common.Hash) (uri string, err error
for len(str) > 0 {
mapaddr := storageMapping(storageIdx2Addr(1), chash[:])
key := storageAddress(storageFixedArray(mapaddr, storageIdx2Addr(idx)))
hex := self.backend.StorageAt(self.urlHintContractAddress, key)
hex := self.backend.StorageAt(UrlHintContractAddress, key)
str = string(common.Hex2Bytes(hex[2:]))
l := len(str)
for (l > 0) && (str[l-1] == 0) {
......@@ -81,7 +184,7 @@ func (self *Resolver) ContentHashToUrl(chash common.Hash) (uri string, err error
}
if len(uri) == 0 {
err = fmt.Errorf("GetURLhint: URL hint not found")
err = fmt.Errorf("GetURLhint: URL hint not found for '%v'", chash.Hex())
}
return
}
......@@ -106,7 +209,8 @@ func storageMapping(addr, key []byte) []byte {
data := make([]byte, 64)
copy(data[0:32], key[0:32])
copy(data[32:64], addr[0:32])
return crypto.Sha3(data)
sha := crypto.Sha3(data)
return sha
}
func storageFixedArray(addr, idx []byte) []byte {
......
......@@ -20,6 +20,8 @@ var (
)
func NewTestBackend() *testBackend {
HashRegContractAddress = common.BigToAddress(common.Big0).Hex()[2:]
UrlHintContractAddress = common.BigToAddress(common.Big1).Hex()[2:]
self := &testBackend{}
self.contracts = make(map[string](map[string]string))
......@@ -27,14 +29,13 @@ func NewTestBackend() *testBackend {
key := storageAddress(storageMapping(storageIdx2Addr(1), codehash[:]))
self.contracts[HashRegContractAddress][key] = hash.Hex()
self.contracts[URLHintContractAddress] = make(map[string]string)
self.contracts[UrlHintContractAddress] = make(map[string]string)
mapaddr := storageMapping(storageIdx2Addr(1), hash[:])
key = storageAddress(storageFixedArray(mapaddr, storageIdx2Addr(0)))
self.contracts[URLHintContractAddress][key] = common.ToHex([]byte(url))
self.contracts[UrlHintContractAddress][key] = common.ToHex([]byte(url))
key = storageAddress(storageFixedArray(mapaddr, storageIdx2Addr(1)))
self.contracts[URLHintContractAddress][key] = "0x00"
self.contracts[UrlHintContractAddress][key] = "0x00"
return self
}
......@@ -47,42 +48,46 @@ func (self *testBackend) StorageAt(ca, sa string) (res string) {
return
}
func (self *testBackend) Transact(fromStr, toStr, nonceStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, error) {
return "", nil
}
func TestKeyToContentHash(t *testing.T) {
b := NewTestBackend()
res := New(b, URLHintContractAddress, HashRegContractAddress)
res := New(b)
got, err := res.KeyToContentHash(codehash)
if err != nil {
t.Errorf("expected no error, got %v", err)
} else {
if got != hash {
t.Errorf("incorrect result, expected %x, got %x: ", hash.Hex(), got.Hex())
t.Errorf("incorrect result, expected '%v', got '%v'", hash.Hex(), got.Hex())
}
}
}
func TestContentHashToUrl(t *testing.T) {
b := NewTestBackend()
res := New(b, URLHintContractAddress, HashRegContractAddress)
res := New(b)
got, err := res.ContentHashToUrl(hash)
if err != nil {
t.Errorf("expected no error, got %v", err)
} else {
if string(got) != url {
t.Errorf("incorrect result, expected %v, got %s: ", url, string(got))
if got != url {
t.Errorf("incorrect result, expected '%v', got '%s'", url, got)
}
}
}
func TestKeyToUrl(t *testing.T) {
b := NewTestBackend()
res := New(b, URLHintContractAddress, HashRegContractAddress)
res := New(b)
got, _, err := res.KeyToUrl(codehash)
if err != nil {
t.Errorf("expected no error, got %v", err)
} else {
if string(got) != url {
t.Errorf("incorrect result, expected %v, got %s: ", url, string(got))
if got != url {
t.Errorf("incorrect result, expected \n'%s', got \n'%s'", url, got)
}
}
}
......@@ -66,7 +66,6 @@ func CalcGasLimit(parent *types.Block) *big.Int {
result := new(big.Int).Add(previous, curInt)
result.Div(result, big.NewInt(1024))
return common.BigMax(params.GenesisGasLimit, result)
}
......@@ -161,7 +160,8 @@ func (self *ChainManager) Td() *big.Int {
}
func (self *ChainManager) GasLimit() *big.Int {
return self.currentGasLimit
// return self.currentGasLimit
return self.currentBlock.GasLimit()
}
func (self *ChainManager) LastBlockHash() common.Hash {
......@@ -280,7 +280,6 @@ func (bc *ChainManager) NewBlock(coinbase common.Address) *types.Block {
header.Difficulty = CalcDifficulty(block.Header(), parent.Header())
header.Number = new(big.Int).Add(parent.Header().Number, common.Big1)
header.GasLimit = CalcGasLimit(parent)
}
return block
......
......@@ -138,9 +138,12 @@ func (c *StateObject) setAddr(addr []byte, value interface{}) {
}
func (self *StateObject) GetStorage(key *big.Int) *common.Value {
fmt.Printf("%v: get %v %v", self.address.Hex(), key)
return self.GetState(common.BytesToHash(key.Bytes()))
}
func (self *StateObject) SetStorage(key *big.Int, value *common.Value) {
fmt.Printf("%v: set %v -> %v", self.address.Hex(), key, value)
self.SetState(common.BytesToHash(key.Bytes()), value)
}
......
......@@ -2,9 +2,8 @@ package rpc
import (
"encoding/json"
"fmt"
"math/big"
"sync"
// "sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
......@@ -14,8 +13,7 @@ import (
)
type EthereumApi struct {
eth *xeth.XEth
xethMu sync.RWMutex
eth *xeth.XEth
}
func NewEthereumApi(xeth *xeth.XEth) *EthereumApi {
......@@ -27,9 +25,6 @@ func NewEthereumApi(xeth *xeth.XEth) *EthereumApi {
}
func (api *EthereumApi) xeth() *xeth.XEth {
api.xethMu.RLock()
defer api.xethMu.RUnlock()
return api.eth
}
......@@ -154,6 +149,7 @@ func (api *EthereumApi) GetRequestReply(req *RpcRequest, reply *interface{}) err
}
*reply = newHexNum(big.NewInt(int64(len(br.Uncles))).Bytes())
case "eth_getData", "eth_getCode":
args := new(GetDataArgs)
if err := json.Unmarshal(req.Params, &args); err != nil {
......@@ -161,18 +157,13 @@ func (api *EthereumApi) GetRequestReply(req *RpcRequest, reply *interface{}) err
}
v := api.xethAtStateNum(args.BlockNumber).CodeAtBytes(args.Address)
*reply = newHexData(v)
case "eth_sendTransaction", "eth_transact":
args := new(NewTxArgs)
if err := json.Unmarshal(req.Params, &args); err != nil {
return err
}
// call ConfirmTransaction first
tx, _ := json.Marshal(req)
if !api.xeth().ConfirmTransaction(string(tx)) {
return fmt.Errorf("Transaction not confirmed")
}
// nonce may be nil ("guess" mode)
var nonce string
if args.Nonce != nil {
......@@ -311,11 +302,36 @@ func (api *EthereumApi) GetRequestReply(req *RpcRequest, reply *interface{}) err
} else {
*reply = v.Uncles[args.Index]
}
case "eth_getCompilers":
c := []string{""}
var lang string
if solc, _ := api.xeth().Solc(); solc != nil {
lang = "Solidity"
}
c := []string{lang}
*reply = c
case "eth_compileSolidity", "eth_compileLLL", "eth_compileSerpent":
case "eth_compileLLL", "eth_compileSerpent":
return NewNotImplementedError(req.Method)
case "eth_compileSolidity":
solc, _ := api.xeth().Solc()
if solc == nil {
return NewNotImplementedError(req.Method)
}
args := new(SourceArgs)
if err := json.Unmarshal(req.Params, &args); err != nil {
return err
}
contract, err := solc.Compile(args.Source)
if err != nil {
return err
}
*reply = contract
case "eth_newFilter":
args := new(BlockFilterArgs)
if err := json.Unmarshal(req.Params, &args); err != nil {
......
......@@ -5,8 +5,12 @@ import (
// "sync"
"testing"
// "time"
// "fmt"
"io/ioutil"
"strconv"
// "github.com/ethereum/go-ethereum/xeth"
"github.com/ethereum/go-ethereum/common/compiler"
"github.com/ethereum/go-ethereum/xeth"
)
func TestWeb3Sha3(t *testing.T) {
......@@ -26,6 +30,97 @@ func TestWeb3Sha3(t *testing.T) {
}
}
func TestCompileSolidity(t *testing.T) {
solc, err := compiler.New("")
if solc == nil {
t.Skip("no solidity compiler")
}
source := `contract test {\n` +
" /// @notice Will multiply `a` by 7." + `\n` +
` function multiply(uint a) returns(uint d) {\n` +
` return a * 7;\n` +
` }\n` +
`}\n`
jsonstr := `{"jsonrpc":"2.0","method":"eth_compileSolidity","params":["` + source + `"],"id":64}`
expCode := "605280600c6000396000f3006000357c010000000000000000000000000000000000000000000000000000000090048063c6888fa114602e57005b60376004356041565b8060005260206000f35b6000600782029050604d565b91905056"
expAbiDefinition := `[{"constant":false,"inputs":[{"name":"a","type":"uint256"}],"name":"multiply","outputs":[{"name":"d","type":"uint256"}],"type":"function"}]`
expUserDoc := `{"methods":{"multiply(uint256)":{"notice":"Will multiply ` + "`a`" + ` by 7."}}}`
expDeveloperDoc := `{"methods":{}}`
expCompilerVersion := `0.9.13`
expLanguage := "Solidity"
expLanguageVersion := "0"
expSource := source
api := NewEthereumApi(&xeth.XEth{})
var req RpcRequest
json.Unmarshal([]byte(jsonstr), &req)
var response interface{}
err = api.GetRequestReply(&req, &response)
if err != nil {
t.Errorf("expected no error, got %v", err)
}
respjson, err := json.Marshal(response)
if err != nil {
t.Errorf("expected no error, got %v", err)
}
var contract = compiler.Contract{}
err = json.Unmarshal(respjson, &contract)
if err != nil {
t.Errorf("expected no error, got %v", err)
}
if contract.Code != expCode {
t.Errorf("Expected %s got %s", expCode, contract.Code)
}
if strconv.Quote(contract.Info.Source) != `"`+expSource+`"` {
t.Errorf("Expected \n'%s' got \n'%s'", expSource, strconv.Quote(contract.Info.Source))
}
if contract.Info.Language != expLanguage {
t.Errorf("Expected %s got %s", expLanguage, contract.Info.Language)
}
if contract.Info.LanguageVersion != expLanguageVersion {
t.Errorf("Expected %s got %s", expLanguageVersion, contract.Info.LanguageVersion)
}
if contract.Info.CompilerVersion != expCompilerVersion {
t.Errorf("Expected %s got %s", expCompilerVersion, contract.Info.CompilerVersion)
}
userdoc, err := json.Marshal(contract.Info.UserDoc)
if err != nil {
t.Errorf("expected no error, got %v", err)
}
devdoc, err := json.Marshal(contract.Info.DeveloperDoc)
if err != nil {
t.Errorf("expected no error, got %v", err)
}
abidef, err := json.Marshal(contract.Info.AbiDefinition)
if err != nil {
t.Errorf("expected no error, got %v", err)
}
if string(abidef) != expAbiDefinition {
t.Errorf("Expected \n'%s' got \n'%s'", expAbiDefinition, string(abidef))
}
ioutil.WriteFile("/tmp/abidef", []byte(string(abidef)), 0700)
ioutil.WriteFile("/tmp/expabidef", []byte(expAbiDefinition), 0700)
if string(userdoc) != expUserDoc {
t.Errorf("Expected \n'%s' got \n'%s'", expUserDoc, string(userdoc))
}
if string(devdoc) != expDeveloperDoc {
t.Errorf("Expected %s got %s", expDeveloperDoc, string(devdoc))
}
}
// func TestDbStr(t *testing.T) {
// jsonput := `{"jsonrpc":"2.0","method":"db_putString","params":["testDB","myKey","myString"],"id":64}`
// jsonget := `{"jsonrpc":"2.0","method":"db_getString","params":["testDB","myKey"],"id":64}`
......
......@@ -1136,3 +1136,26 @@ func (args *SubmitWorkArgs) UnmarshalJSON(b []byte) (err error) {
return nil
}
type SourceArgs struct {
Source string
}
func (args *SourceArgs) UnmarshalJSON(b []byte) (err error) {
var obj []interface{}
if err := json.Unmarshal(b, &obj); err != nil {
return NewDecodeParamError(err.Error())
}
if len(obj) < 1 {
return NewInsufficientParamsError(len(obj), 1)
}
arg0, ok := obj[0].(string)
if !ok {
return NewInvalidTypeError("source code", "not a string")
}
args.Source = arg0
return nil
}
......@@ -2494,3 +2494,13 @@ func TestBlockHeightFromJsonInvalid(t *testing.T) {
t.Error(str)
}
}
func TestSourceArgsEmpty(t *testing.T) {
input := `[]`
args := new(SourceArgs)
str := ExpectInsufficientParamsError(json.Unmarshal([]byte(input), &args))
if len(str) > 0 {
t.Error(str)
}
}
......@@ -11,6 +11,7 @@ import (
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/compiler"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
......@@ -65,6 +66,9 @@ type XEth struct {
// regmut sync.Mutex
// register map[string][]*interface{} // TODO improve return type
solcPath string
solc *compiler.Solidity
agent *miner.RemoteAgent
}
......@@ -85,7 +89,6 @@ func New(eth *eth.Ethereum, frontend Frontend) *XEth {
agent: miner.NewRemoteAgent(),
}
eth.Miner().Register(xeth.agent)
if frontend == nil {
xeth.frontend = dummyFrontend{}
}
......@@ -185,9 +188,38 @@ func (self *XEth) AtStateNum(num int64) *XEth {
return self.WithState(st)
}
// applies queued transactions originating from address onto the latest state
// and creates a block
// only used in tests
// - could be removed in favour of mining on testdag (natspec e2e + networking)
// + filters
func (self *XEth) ApplyTestTxs(statedb *state.StateDB, address common.Address, txc uint64) (uint64, *XEth) {
block := self.backend.ChainManager().NewBlock(address)
coinbase := statedb.GetStateObject(address)
coinbase.SetGasPool(big.NewInt(10000000))
txs := self.backend.TxPool().GetQueuedTransactions()
for i := 0; i < len(txs); i++ {
for _, tx := range txs {
if tx.Nonce() == txc {
_, _, err := core.ApplyMessage(core.NewEnv(statedb, self.backend.ChainManager(), tx, block), tx, coinbase)
if err != nil {
panic(err)
}
txc++
}
}
}
xeth := self.WithState(statedb)
return txc, xeth
}
func (self *XEth) WithState(statedb *state.StateDB) *XEth {
xeth := &XEth{
backend: self.backend,
backend: self.backend,
frontend: self.frontend,
}
xeth.state = NewState(xeth, statedb)
......@@ -196,6 +228,44 @@ func (self *XEth) WithState(statedb *state.StateDB) *XEth {
func (self *XEth) State() *State { return self.state }
// subscribes to new head block events and
// waits until blockchain height is greater n at any time
// given the current head, waits for the next chain event
// sets the state to the current head
// loop is async and quit by closing the channel
// used in tests and JS console debug module to control advancing private chain manually
// Note: this is not threadsafe, only called in JS single process and tests
func (self *XEth) UpdateState() (wait chan *big.Int) {
wait = make(chan *big.Int)
go func() {
sub := self.backend.EventMux().Subscribe(core.ChainHeadEvent{})
var m, n *big.Int
var ok bool
out:
for {
select {
case event := <-sub.Chan():
ev, ok := event.(core.ChainHeadEvent)
if ok {
m = ev.Block.Number()
if n != nil && n.Cmp(m) < 0 {
wait <- n
n = nil
}
statedb := state.New(ev.Block.Root(), self.backend.StateDb())
self.state = NewState(self, statedb)
}
case n, ok = <-wait:
if !ok {
break out
}
}
}
sub.Unsubscribe()
}()
return
}
func (self *XEth) Whisper() *Whisper { return self.whisper }
func (self *XEth) getBlockByHeight(height int64) *types.Block {
......@@ -298,6 +368,23 @@ func (self *XEth) Accounts() []string {
return accountAddresses
}
// accessor for solidity compiler.
// memoized if available, retried on-demand if not
func (self *XEth) Solc() (*compiler.Solidity, error) {
var err error
if self.solc == nil {
self.solc, err = compiler.New(self.solcPath)
}
return self.solc, err
}
// set in js console via admin interface or wrapper from cli flags
func (self *XEth) SetSolc(solcPath string) (*compiler.Solidity, error) {
self.solcPath = solcPath
self.solc = nil
return self.Solc()
}
func (self *XEth) DbPut(key, val []byte) bool {
self.backend.ExtraDb().Put(key, val)
return true
......@@ -723,12 +810,18 @@ func (self *XEth) Call(fromStr, toStr, valueStr, gasStr, gasPriceStr, dataStr st
}
func (self *XEth) ConfirmTransaction(tx string) bool {
return self.frontend.ConfirmTransaction(tx)
}
func (self *XEth) Transact(fromStr, toStr, nonceStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, error) {
// this minimalistic recoding is enough (works for natspec.js)
var jsontx = fmt.Sprintf(`{"params":[{"to":"%s","data": "%s"}]}`, toStr, codeStr)
if !self.ConfirmTransaction(jsontx) {
err := fmt.Errorf("Transaction not confirmed")
return "", err
}
var (
from = common.HexToAddress(fromStr)
to = common.HexToAddress(toStr)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment