package natspec import ( "bytes" "encoding/json" "fmt" "github.com/robertkrimen/otto" "strings" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/docserver" "github.com/ethereum/go-ethereum/common/resolver" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/xeth" ) type abi2method map[[8]byte]*method type NatSpec struct { 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(fmt.Sprintf("no NatSpec info found for contract: %v", err), tx) } else { return getFallbackNotice(fmt.Sprintf("invalid NatSpec info: %v", err), 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) } type transaction struct { To string `json:"to"` Data string `json:"data"` } 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"` } 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 } t := tx.Params[0] contractAddress := t.To content, err := FetchDocsForContract(contractAddress, xeth, http) if err != nil { return } 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 codehex := xeth.CodeAt(contractAddress) 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) // resolve host via HashReg/UrlHint Resolver 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 } return } func NewWithDocs(infoDoc []byte, tx string, data string) (self *NatSpec, err error) { var contract contractInfo err = json.Unmarshal(infoDoc, &contract) if err != nil { return } self = &NatSpec{ 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) _, err = self.jsvm.Run(natspecJS) if err != nil { return } _, err = self.jsvm.Run("var natspec = require('natspec');") return } // type abiDoc []method // type method struct { // Name string `json:name` // Inputs []input `json:inputs` // abiKey [8]byte // } // type input struct { // Name string `json:name` // Type string `json:type` // } // json skeleton for abi doc (contract method definitions) type method struct { Notice string `json:notice` name string } type userDoc struct { Methods map[string]*method `json:methods` } func (self *NatSpec) makeAbi2method(abiKey [8]byte) (meth *method) { for signature, m := range self.userDoc.Methods { name := strings.Split(signature, "(")[0] hash := []byte(common.Bytes2Hex(crypto.Sha3([]byte(signature)))) var key [8]byte copy(key[:], hash[:8]) if bytes.Equal(key[:], abiKey[:]) { meth = m meth.name = name return } } return } func (self *NatSpec) Notice() (notice string, err error) { var abiKey [8]byte if len(self.data) < 10 { err = fmt.Errorf("Invalid transaction data") return } copy(abiKey[:], self.data[2:10]) meth := self.makeAbi2method(abiKey) if meth == nil { err = fmt.Errorf("abi key does not match any method") return } notice, err = self.noticeForMethod(self.tx, meth.name, meth.Notice) return } func (self *NatSpec) noticeForMethod(tx string, name, expression string) (notice string, err error) { if _, err = self.jsvm.Run("var transaction = " + tx + ";"); err != nil { return "", fmt.Errorf("natspec.js error setting transaction: %v", err) } if _, err = self.jsvm.Run("var abi = " + string(self.abiDocJson) + ";"); err != nil { return "", fmt.Errorf("natspec.js error setting abi: %v", err) } if _, err = self.jsvm.Run("var method = '" + name + "';"); err != nil { return "", fmt.Errorf("natspec.js error setting method: %v", err) } if _, err = self.jsvm.Run("var expression = \"" + expression + "\";"); err != nil { return "", fmt.Errorf("natspec.js error setting expression: %v", err) } self.jsvm.Run("var call = {method: method,abi: abi,transaction: transaction};") value, err := self.jsvm.Run("natspec.evaluateExpression(expression, call);") if err != nil { return "", fmt.Errorf("natspec.js error evaluating expression: %v", err) } evalError := "Natspec evaluation failed, wrong input params" if value.String() == evalError { return "", fmt.Errorf("natspec.js error evaluating expression: wrong input params in expression '%s'", expression) } if len(value.String()) == 0 { return "", fmt.Errorf("natspec.js error evaluating expression") } return value.String(), nil }