Merge pull request #2359 from bas-vk/rpc-optional-args

rpc: several fixes and support for optional arguments
This commit is contained in:
Jeffrey Wilcke 2016-04-12 14:03:21 +02:00
commit 8627680e24
23 changed files with 3440 additions and 648 deletions

View File

@ -238,7 +238,7 @@ func (js *jsre) apiBindings() error {
} }
// load only supported API's in javascript runtime // load only supported API's in javascript runtime
shortcuts := "var eth = web3.eth; " shortcuts := "var eth = web3.eth; var personal = web3.personal; "
for _, apiName := range apiNames { for _, apiName := range apiNames {
if apiName == "web3" || apiName == "rpc" { if apiName == "web3" || apiName == "rpc" {
continue // manually mapped or ignore continue // manually mapped or ignore

View File

@ -326,7 +326,7 @@ JavaScript API. See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Conso
utils.WSListenAddrFlag, utils.WSListenAddrFlag,
utils.WSPortFlag, utils.WSPortFlag,
utils.WSApiFlag, utils.WSApiFlag,
utils.WSAllowedDomainsFlag, utils.WSAllowedOriginsFlag,
utils.IPCDisabledFlag, utils.IPCDisabledFlag,
utils.IPCApiFlag, utils.IPCApiFlag,
utils.IPCPathFlag, utils.IPCPathFlag,

View File

@ -94,7 +94,7 @@ var AppHelpFlagGroups = []flagGroup{
utils.WSListenAddrFlag, utils.WSListenAddrFlag,
utils.WSPortFlag, utils.WSPortFlag,
utils.WSApiFlag, utils.WSApiFlag,
utils.WSAllowedDomainsFlag, utils.WSAllowedOriginsFlag,
utils.IPCDisabledFlag, utils.IPCDisabledFlag,
utils.IPCApiFlag, utils.IPCApiFlag,
utils.IPCPathFlag, utils.IPCPathFlag,

View File

@ -287,9 +287,9 @@ var (
Usage: "API's offered over the WS-RPC interface", Usage: "API's offered over the WS-RPC interface",
Value: rpc.DefaultHTTPApis, Value: rpc.DefaultHTTPApis,
} }
WSAllowedDomainsFlag = cli.StringFlag{ WSAllowedOriginsFlag = cli.StringFlag{
Name: "wsdomains", Name: "wsorigins",
Usage: "Domains from which to accept websockets requests (can be spoofed)", Usage: "Origins from which to accept websockets requests",
Value: "", Value: "",
} }
ExecFlag = cli.StringFlag{ ExecFlag = cli.StringFlag{
@ -659,7 +659,7 @@ func MakeSystemNode(name, version string, extra []byte, ctx *cli.Context) *node.
HTTPModules: strings.Split(ctx.GlobalString(RPCApiFlag.Name), ","), HTTPModules: strings.Split(ctx.GlobalString(RPCApiFlag.Name), ","),
WSHost: MakeWSRpcHost(ctx), WSHost: MakeWSRpcHost(ctx),
WSPort: ctx.GlobalInt(WSPortFlag.Name), WSPort: ctx.GlobalInt(WSPortFlag.Name),
WSDomains: ctx.GlobalString(WSAllowedDomainsFlag.Name), WSOrigins: ctx.GlobalString(WSAllowedOriginsFlag.Name),
WSModules: strings.Split(ctx.GlobalString(WSApiFlag.Name), ","), WSModules: strings.Split(ctx.GlobalString(WSApiFlag.Name), ","),
} }
// Configure the Ethereum service // Configure the Ethereum service

View File

@ -37,7 +37,8 @@ func NewJeth(re *jsre.JSRE, client rpc.Client) *Jeth {
return &Jeth{re, client} return &Jeth{re, client}
} }
func (self *Jeth) err(call otto.FunctionCall, code int, msg string, id *int64) (response otto.Value) { // err returns an error object for the given error code and message.
func (self *Jeth) err(call otto.FunctionCall, code int, msg string, id interface{}) (response otto.Value) {
m := rpc.JSONErrResponse{ m := rpc.JSONErrResponse{
Version: "2.0", Version: "2.0",
Id: id, Id: id,
@ -56,44 +57,50 @@ func (self *Jeth) err(call otto.FunctionCall, code int, msg string, id *int64) (
return res return res
} }
// UnlockAccount asks the user for the password and than executes the jeth.UnlockAccount callback in the jsre // UnlockAccount asks the user for the password and than executes the jeth.UnlockAccount callback in the jsre.
// It will need the public address for the account to unlock as first argument.
// The second argument is an optional string with the password. If not given the user is prompted for the password.
// The third argument is an optional integer which specifies for how long the account will be unlocked (in seconds).
func (self *Jeth) UnlockAccount(call otto.FunctionCall) (response otto.Value) { func (self *Jeth) UnlockAccount(call otto.FunctionCall) (response otto.Value) {
var account, passwd string var account, passwd otto.Value
timeout := int64(300) duration := otto.NullValue()
var ok bool
if len(call.ArgumentList) == 0 { if !call.Argument(0).IsString() {
fmt.Println("expected address of account to unlock") fmt.Println("first argument must be the account to unlock")
return otto.FalseValue() return otto.FalseValue()
} }
if len(call.ArgumentList) >= 1 { account = call.Argument(0)
if accountExport, err := call.Argument(0).Export(); err == nil {
if account, ok = accountExport.(string); ok { // if password is not given or as null value -> ask user for password
if len(call.ArgumentList) == 1 { if call.Argument(1).IsUndefined() || call.Argument(1).IsNull() {
fmt.Printf("Unlock account %s\n", account) fmt.Printf("Unlock account %s\n", account)
passwd, err = PromptPassword("Passphrase: ", true) if password, err := PromptPassword("Passphrase: ", true); err == nil {
if err != nil { passwd, _ = otto.ToValue(password)
return otto.FalseValue() } else {
throwJSExeception(err.Error())
} }
} else {
if !call.Argument(1).IsString() {
throwJSExeception("password must be a string")
} }
} passwd = call.Argument(1)
}
}
if len(call.ArgumentList) >= 2 {
if passwdExport, err := call.Argument(1).Export(); err == nil {
passwd, _ = passwdExport.(string)
}
} }
if len(call.ArgumentList) >= 3 { // third argument is the duration how long the account must be unlocked.
if timeoutExport, err := call.Argument(2).Export(); err == nil { // verify that its a number.
timeout, _ = timeoutExport.(int64) if call.Argument(2).IsDefined() && !call.Argument(2).IsNull() {
if !call.Argument(2).IsNumber() {
throwJSExeception("unlock duration must be a number")
} }
duration = call.Argument(2)
} }
if val, err := call.Otto.Call("jeth.unlockAccount", nil, account, passwd, timeout); err == nil { // jeth.unlockAccount will send the request to the backend.
if val, err := call.Otto.Call("jeth.unlockAccount", nil, account, passwd, duration); err == nil {
return val return val
} else {
throwJSExeception(err.Error())
} }
return otto.FalseValue() return otto.FalseValue()
@ -134,19 +141,31 @@ func (self *Jeth) NewAccount(call otto.FunctionCall) (response otto.Value) {
return otto.FalseValue() return otto.FalseValue()
} }
// Send will serialize the first argument, send it to the node and returns the response.
func (self *Jeth) Send(call otto.FunctionCall) (response otto.Value) { func (self *Jeth) Send(call otto.FunctionCall) (response otto.Value) {
reqif, err := call.Argument(0).Export() // verify we got a batch request (array) or a single request (object)
if err != nil { ro := call.Argument(0).Object()
return self.err(call, -32700, err.Error(), nil) if ro == nil || (ro.Class() != "Array" && ro.Class() != "Object") {
throwJSExeception("Internal Error: request must be an object or array")
} }
jsonreq, err := json.Marshal(reqif) // convert otto vm arguments to go values by JSON serialising and parsing.
data, err := call.Otto.Call("JSON.stringify", nil, ro)
if err != nil {
throwJSExeception(err.Error())
}
jsonreq, _ := data.ToString()
// parse arguments to JSON rpc requests, either to an array (batch) or to a single request.
var reqs []rpc.JSONRequest var reqs []rpc.JSONRequest
batch := true batch := true
err = json.Unmarshal(jsonreq, &reqs) if err = json.Unmarshal([]byte(jsonreq), &reqs); err != nil {
if err != nil { // single request?
reqs = make([]rpc.JSONRequest, 1) reqs = make([]rpc.JSONRequest, 1)
err = json.Unmarshal(jsonreq, &reqs[0]) if err = json.Unmarshal([]byte(jsonreq), &reqs[0]); err != nil {
throwJSExeception("invalid request")
}
batch = false batch = false
} }
@ -154,47 +173,50 @@ func (self *Jeth) Send(call otto.FunctionCall) (response otto.Value) {
call.Otto.Run("var ret_response = new Array(response_len);") call.Otto.Run("var ret_response = new Array(response_len);")
for i, req := range reqs { for i, req := range reqs {
err := self.client.Send(&req) if err := self.client.Send(&req); err != nil {
if err != nil {
return self.err(call, -32603, err.Error(), req.Id) return self.err(call, -32603, err.Error(), req.Id)
} }
result := make(map[string]interface{}) result := make(map[string]interface{})
err = self.client.Recv(&result) if err = self.client.Recv(&result); err != nil {
if err != nil {
return self.err(call, -32603, err.Error(), req.Id) return self.err(call, -32603, err.Error(), req.Id)
} }
_, isSuccessResponse := result["result"]
_, isErrorResponse := result["error"]
if !isSuccessResponse && !isErrorResponse {
return self.err(call, -32603, fmt.Sprintf("Invalid response"), new(int64))
}
id, _ := result["id"] id, _ := result["id"]
call.Otto.Set("ret_id", id)
jsonver, _ := result["jsonrpc"] jsonver, _ := result["jsonrpc"]
call.Otto.Set("ret_jsonrpc", jsonver)
var payload []byte call.Otto.Set("ret_id", id)
if isSuccessResponse { call.Otto.Set("ret_jsonrpc", jsonver)
payload, _ = json.Marshal(result["result"])
} else if isErrorResponse {
payload, _ = json.Marshal(result["error"])
}
call.Otto.Set("ret_result", string(payload))
call.Otto.Set("response_idx", i) call.Otto.Set("response_idx", i)
// call was successful
if res, ok := result["result"]; ok {
payload, _ := json.Marshal(res)
call.Otto.Set("ret_result", string(payload))
response, err = call.Otto.Run(` response, err = call.Otto.Run(`
ret_response[response_idx] = { jsonrpc: ret_jsonrpc, id: ret_id, result: JSON.parse(ret_result) }; ret_response[response_idx] = { jsonrpc: ret_jsonrpc, id: ret_id, result: JSON.parse(ret_result) };
`) `)
continue
}
// request returned an error
if res, ok := result["error"]; ok {
payload, _ := json.Marshal(res)
call.Otto.Set("ret_result", string(payload))
response, err = call.Otto.Run(`
ret_response[response_idx] = { jsonrpc: ret_jsonrpc, id: ret_id, error: JSON.parse(ret_result) };
`)
continue
}
return self.err(call, -32603, fmt.Sprintf("Invalid response"), new(int64))
} }
if !batch { if !batch {
call.Otto.Run("ret_response = ret_response[0];") call.Otto.Run("ret_response = ret_response[0];")
} }
// if a callback was given execute it.
if call.Argument(1).IsObject() { if call.Argument(1).IsObject() {
call.Otto.Set("callback", call.Argument(1)) call.Otto.Set("callback", call.Argument(1))
call.Otto.Run(` call.Otto.Run(`
@ -207,53 +229,6 @@ func (self *Jeth) Send(call otto.FunctionCall) (response otto.Value) {
return return
} }
/*
// handleRequest will handle user agent requests by interacting with the user and sending
// the user response back to the geth service
func (self *Jeth) handleRequest(req *shared.Request) bool {
var err error
var args []interface{}
if err = json.Unmarshal(req.Params, &args); err != nil {
glog.V(logger.Info).Infof("Unable to parse agent request - %v\n", err)
return false
}
switch req.Method {
case useragent.AskPasswordMethod:
return self.askPassword(req.Id, req.Jsonrpc, args)
case useragent.ConfirmTransactionMethod:
return self.confirmTransaction(req.Id, req.Jsonrpc, args)
}
return false
}
// askPassword will ask the user to supply the password for a given account
func (self *Jeth) askPassword(id interface{}, jsonrpc string, args []interface{}) bool {
var err error
var passwd string
if len(args) >= 1 {
if account, ok := args[0].(string); ok {
fmt.Printf("Unlock account %s\n", account)
} else {
return false
}
}
passwd, err = PromptPassword("Passphrase: ", true)
if err = self.client.Send(shared.NewRpcResponse(id, jsonrpc, passwd, err)); err != nil {
glog.V(logger.Info).Infof("Unable to send user agent ask password response - %v\n", err)
}
return err == nil
}
func (self *Jeth) confirmTransaction(id interface{}, jsonrpc string, args []interface{}) bool {
// Accept all tx which are send from this console
return self.client.Send(shared.NewRpcResponse(id, jsonrpc, true, nil)) == nil
}
*/
// throwJSExeception panics on an otto value, the Otto VM will then throw msg as a javascript error. // throwJSExeception panics on an otto value, the Otto VM will then throw msg as a javascript error.
func throwJSExeception(msg interface{}) otto.Value { func throwJSExeception(msg interface{}) otto.Value {
p, _ := otto.ToValue(msg) p, _ := otto.ToValue(msg)

View File

@ -25,6 +25,7 @@ import (
"io/ioutil" "io/ioutil"
"math/big" "math/big"
"os" "os"
"runtime"
"sync" "sync"
"time" "time"
@ -96,8 +97,8 @@ type PublicEthereumAPI struct {
} }
// NewPublicEthereumAPI creates a new Ethereum protocol API. // NewPublicEthereumAPI creates a new Ethereum protocol API.
func NewPublicEthereumAPI(e *Ethereum) *PublicEthereumAPI { func NewPublicEthereumAPI(e *Ethereum, gpo *GasPriceOracle) *PublicEthereumAPI {
return &PublicEthereumAPI{e, NewGasPriceOracle(e)} return &PublicEthereumAPI{e, gpo}
} }
// GasPrice returns a suggestion for a gas price. // GasPrice returns a suggestion for a gas price.
@ -108,11 +109,7 @@ func (s *PublicEthereumAPI) GasPrice() *big.Int {
// GetCompilers returns the collection of available smart contract compilers // GetCompilers returns the collection of available smart contract compilers
func (s *PublicEthereumAPI) GetCompilers() ([]string, error) { func (s *PublicEthereumAPI) GetCompilers() ([]string, error) {
solc, err := s.e.Solc() solc, err := s.e.Solc()
if err != nil { if err != nil && solc != nil {
return nil, err
}
if solc != nil {
return []string{"Solidity"}, nil return []string{"Solidity"}, nil
} }
@ -240,9 +237,15 @@ func NewPrivateMinerAPI(e *Ethereum) *PrivateMinerAPI {
return &PrivateMinerAPI{e: e} return &PrivateMinerAPI{e: e}
} }
// Start the miner with the given number of threads // Start the miner with the given number of threads. If threads is nil the number of
func (s *PrivateMinerAPI) Start(threads rpc.HexNumber) (bool, error) { // workers started is equal to the number of logical CPU's that are usable by this process.
func (s *PrivateMinerAPI) Start(threads *rpc.HexNumber) (bool, error) {
s.e.StartAutoDAG() s.e.StartAutoDAG()
if threads == nil {
threads = rpc.NewHexNumber(runtime.NumCPU())
}
err := s.e.StartMining(threads.Int(), "") err := s.e.StartMining(threads.Int(), "")
if err == nil { if err == nil {
return true, nil return true, nil
@ -265,7 +268,7 @@ func (s *PrivateMinerAPI) SetExtra(extra string) (bool, error) {
} }
// SetGasPrice sets the minimum accepted gas price for the miner. // SetGasPrice sets the minimum accepted gas price for the miner.
func (s *PrivateMinerAPI) SetGasPrice(gasPrice rpc.Number) bool { func (s *PrivateMinerAPI) SetGasPrice(gasPrice rpc.HexNumber) bool {
s.e.Miner().SetGasPrice(gasPrice.BigInt()) s.e.Miner().SetGasPrice(gasPrice.BigInt())
return true return true
} }
@ -440,10 +443,15 @@ func (s *PrivateAccountAPI) NewAccount(password string) (common.Address, error)
return common.Address{}, err return common.Address{}, err
} }
// UnlockAccount will unlock the account associated with the given address with the given password for duration seconds. // UnlockAccount will unlock the account associated with the given address with
// It returns an indication if the action was successful. // the given password for duration seconds. If duration is nil it will use a
func (s *PrivateAccountAPI) UnlockAccount(addr common.Address, password string, duration int) bool { // default of 300 seconds. It returns an indication if the account was unlocked.
if err := s.am.TimedUnlock(addr, password, time.Duration(duration)*time.Second); err != nil { func (s *PrivateAccountAPI) UnlockAccount(addr common.Address, password string, duration *rpc.HexNumber) bool {
if duration == nil {
duration = rpc.NewHexNumber(300)
}
if err := s.am.TimedUnlock(addr, password, time.Duration(duration.Int())*time.Second); err != nil {
glog.V(logger.Info).Infof("%v\n", err) glog.V(logger.Info).Infof("%v\n", err)
return false return false
} }
@ -466,10 +474,11 @@ type PublicBlockChainAPI struct {
newBlockSubscriptions map[string]func(core.ChainEvent) error // callbacks for new block subscriptions newBlockSubscriptions map[string]func(core.ChainEvent) error // callbacks for new block subscriptions
am *accounts.Manager am *accounts.Manager
miner *miner.Miner miner *miner.Miner
gpo *GasPriceOracle
} }
// NewPublicBlockChainAPI creates a new Etheruem blockchain API. // NewPublicBlockChainAPI creates a new Etheruem blockchain API.
func NewPublicBlockChainAPI(config *core.ChainConfig, bc *core.BlockChain, m *miner.Miner, chainDb ethdb.Database, eventMux *event.TypeMux, am *accounts.Manager) *PublicBlockChainAPI { func NewPublicBlockChainAPI(config *core.ChainConfig, bc *core.BlockChain, m *miner.Miner, chainDb ethdb.Database, gpo *GasPriceOracle, eventMux *event.TypeMux, am *accounts.Manager) *PublicBlockChainAPI {
api := &PublicBlockChainAPI{ api := &PublicBlockChainAPI{
config: config, config: config,
bc: bc, bc: bc,
@ -478,6 +487,7 @@ func NewPublicBlockChainAPI(config *core.ChainConfig, bc *core.BlockChain, m *mi
eventMux: eventMux, eventMux: eventMux,
am: am, am: am,
newBlockSubscriptions: make(map[string]func(core.ChainEvent) error), newBlockSubscriptions: make(map[string]func(core.ChainEvent) error),
gpo: gpo,
} }
go api.subscriptionLoop() go api.subscriptionLoop()
@ -674,8 +684,8 @@ func (m callmsg) Data() []byte { return m.data }
type CallArgs struct { type CallArgs struct {
From common.Address `json:"from"` From common.Address `json:"from"`
To *common.Address `json:"to"` To *common.Address `json:"to"`
Gas rpc.HexNumber `json:"gas"` Gas *rpc.HexNumber `json:"gas"`
GasPrice rpc.HexNumber `json:"gasPrice"` GasPrice *rpc.HexNumber `json:"gasPrice"`
Value rpc.HexNumber `json:"value"` Value rpc.HexNumber `json:"value"`
Data string `json:"data"` Data string `json:"data"`
} }
@ -711,11 +721,11 @@ func (s *PublicBlockChainAPI) doCall(args CallArgs, blockNr rpc.BlockNumber) (st
value: args.Value.BigInt(), value: args.Value.BigInt(),
data: common.FromHex(args.Data), data: common.FromHex(args.Data),
} }
if msg.gas.Cmp(common.Big0) == 0 { if msg.gas == nil {
msg.gas = big.NewInt(50000000) msg.gas = big.NewInt(50000000)
} }
if msg.gasPrice.Cmp(common.Big0) == 0 { if msg.gasPrice == nil {
msg.gasPrice = new(big.Int).Mul(big.NewInt(50), common.Shannon) msg.gasPrice = s.gpo.SuggestPrice()
} }
// Execute the call and return // Execute the call and return
@ -882,10 +892,10 @@ type PublicTransactionPoolAPI struct {
} }
// NewPublicTransactionPoolAPI creates a new RPC service with methods specific for the transaction pool. // NewPublicTransactionPoolAPI creates a new RPC service with methods specific for the transaction pool.
func NewPublicTransactionPoolAPI(e *Ethereum) *PublicTransactionPoolAPI { func NewPublicTransactionPoolAPI(e *Ethereum, gpo *GasPriceOracle) *PublicTransactionPoolAPI {
api := &PublicTransactionPoolAPI{ api := &PublicTransactionPoolAPI{
eventMux: e.EventMux(), eventMux: e.EventMux(),
gpo: NewGasPriceOracle(e), gpo: gpo,
chainDb: e.ChainDb(), chainDb: e.ChainDb(),
bc: e.BlockChain(), bc: e.BlockChain(),
am: e.AccountManager(), am: e.AccountManager(),
@ -1306,7 +1316,7 @@ func newTx(t *types.Transaction) *Tx {
// SignTransaction will sign the given transaction with the from account. // SignTransaction will sign the given transaction with the from account.
// The node needs to have the private key of the account corresponding with // The node needs to have the private key of the account corresponding with
// the given from address and it needs to be unlocked. // the given from address and it needs to be unlocked.
func (s *PublicTransactionPoolAPI) SignTransaction(args *SignTransactionArgs) (*SignTransactionResult, error) { func (s *PublicTransactionPoolAPI) SignTransaction(args SignTransactionArgs) (*SignTransactionResult, error) {
if args.Gas == nil { if args.Gas == nil {
args.Gas = rpc.NewHexNumber(defaultGas) args.Gas = rpc.NewHexNumber(defaultGas)
} }
@ -1397,7 +1407,7 @@ func (s *PublicTransactionPoolAPI) NewPendingTransactions(ctx context.Context) (
// Resend accepts an existing transaction and a new gas price and limit. It will remove the given transaction from the // Resend accepts an existing transaction and a new gas price and limit. It will remove the given transaction from the
// pool and reinsert it with the new gas price and limit. // pool and reinsert it with the new gas price and limit.
func (s *PublicTransactionPoolAPI) Resend(tx *Tx, gasPrice, gasLimit *rpc.HexNumber) (common.Hash, error) { func (s *PublicTransactionPoolAPI) Resend(tx Tx, gasPrice, gasLimit *rpc.HexNumber) (common.Hash, error) {
pending := s.txPool.GetTransactions() pending := s.txPool.GetTransactions()
for _, p := range pending { for _, p := range pending {

View File

@ -272,11 +272,14 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {
// APIs returns the collection of RPC services the ethereum package offers. // APIs returns the collection of RPC services the ethereum package offers.
// NOTE, some of these services probably need to be moved to somewhere else. // NOTE, some of these services probably need to be moved to somewhere else.
func (s *Ethereum) APIs() []rpc.API { func (s *Ethereum) APIs() []rpc.API {
// share gas price oracle in API's
gpo := NewGasPriceOracle(s)
return []rpc.API{ return []rpc.API{
{ {
Namespace: "eth", Namespace: "eth",
Version: "1.0", Version: "1.0",
Service: NewPublicEthereumAPI(s), Service: NewPublicEthereumAPI(s, gpo),
Public: true, Public: true,
}, { }, {
Namespace: "eth", Namespace: "eth",
@ -291,12 +294,12 @@ func (s *Ethereum) APIs() []rpc.API {
}, { }, {
Namespace: "eth", Namespace: "eth",
Version: "1.0", Version: "1.0",
Service: NewPublicBlockChainAPI(s.chainConfig, s.BlockChain(), s.Miner(), s.ChainDb(), s.EventMux(), s.AccountManager()), Service: NewPublicBlockChainAPI(s.chainConfig, s.BlockChain(), s.Miner(), s.ChainDb(), gpo, s.EventMux(), s.AccountManager()),
Public: true, Public: true,
}, { }, {
Namespace: "eth", Namespace: "eth",
Version: "1.0", Version: "1.0",
Service: NewPublicTransactionPoolAPI(s), Service: NewPublicTransactionPoolAPI(s, gpo),
Public: true, Public: true,
}, { }, {
Namespace: "eth", Namespace: "eth",

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,7 @@ import (
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/rpc"
"github.com/rcrowley/go-metrics" "github.com/rcrowley/go-metrics"
) )
@ -58,14 +59,33 @@ func (api *PrivateAdminAPI) AddPeer(url string) (bool, error) {
} }
// StartRPC starts the HTTP RPC API server. // StartRPC starts the HTTP RPC API server.
func (api *PrivateAdminAPI) StartRPC(host string, port int, cors string, apis string) (bool, error) { func (api *PrivateAdminAPI) StartRPC(host *string, port *rpc.HexNumber, cors *string, apis *string) (bool, error) {
api.node.lock.Lock() api.node.lock.Lock()
defer api.node.lock.Unlock() defer api.node.lock.Unlock()
if api.node.httpHandler != nil { if api.node.httpHandler != nil {
return false, fmt.Errorf("HTTP RPC already running on %s", api.node.httpEndpoint) return false, fmt.Errorf("HTTP RPC already running on %s", api.node.httpEndpoint)
} }
if err := api.node.startHTTP(fmt.Sprintf("%s:%d", host, port), api.node.rpcAPIs, strings.Split(apis, ","), cors); err != nil {
if host == nil {
host = &api.node.httpHost
}
if port == nil {
port = rpc.NewHexNumber(api.node.httpPort)
}
if cors == nil {
cors = &api.node.httpCors
}
modules := api.node.httpWhitelist
if apis != nil {
modules = nil
for _, m := range strings.Split(*apis, ",") {
modules = append(modules, strings.TrimSpace(m))
}
}
if err := api.node.startHTTP(fmt.Sprintf("%s:%d", *host, port.Int()), api.node.rpcAPIs, modules, *cors); err != nil {
return false, err return false, err
} }
return true, nil return true, nil
@ -84,14 +104,33 @@ func (api *PrivateAdminAPI) StopRPC() (bool, error) {
} }
// StartWS starts the websocket RPC API server. // StartWS starts the websocket RPC API server.
func (api *PrivateAdminAPI) StartWS(host string, port int, cors string, apis string) (bool, error) { func (api *PrivateAdminAPI) StartWS(host *string, port *rpc.HexNumber, allowedOrigins *string, apis *string) (bool, error) {
api.node.lock.Lock() api.node.lock.Lock()
defer api.node.lock.Unlock() defer api.node.lock.Unlock()
if api.node.wsHandler != nil { if api.node.wsHandler != nil {
return false, fmt.Errorf("WebSocket RPC already running on %s", api.node.wsEndpoint) return false, fmt.Errorf("WebSocket RPC already running on %s", api.node.wsEndpoint)
} }
if err := api.node.startWS(fmt.Sprintf("%s:%d", host, port), api.node.rpcAPIs, strings.Split(apis, ","), cors); err != nil {
if host == nil {
host = &api.node.wsHost
}
if port == nil {
port = rpc.NewHexNumber(api.node.wsPort)
}
if allowedOrigins == nil {
allowedOrigins = &api.node.wsOrigins
}
modules := api.node.wsWhitelist
if apis != nil {
modules = nil
for _, m := range strings.Split(*apis, ",") {
modules = append(modules, strings.TrimSpace(m))
}
}
if err := api.node.startWS(fmt.Sprintf("%s:%d", *host, port.Int()), api.node.rpcAPIs, modules, *allowedOrigins); err != nil {
return false, err return false, err
} }
return true, nil return true, nil

View File

@ -127,10 +127,10 @@ type Config struct {
// ephemeral nodes). // ephemeral nodes).
WSPort int WSPort int
// WSDomains is the list of domain to accept websocket requests from. Please be // WSOrigins is the list of domain to accept websocket requests from. Please be
// aware that the server can only act upon the HTTP request the client sends and // aware that the server can only act upon the HTTP request the client sends and
// cannot verify the validity of the request header. // cannot verify the validity of the request header.
WSDomains string WSOrigins string
// WSModules is a list of API modules to expose via the websocket RPC interface. // WSModules is a list of API modules to expose via the websocket RPC interface.
// If the module list is empty, all RPC API endpoints designated public will be // If the module list is empty, all RPC API endpoints designated public will be

View File

@ -62,15 +62,19 @@ type Node struct {
ipcListener net.Listener // IPC RPC listener socket to serve API requests ipcListener net.Listener // IPC RPC listener socket to serve API requests
ipcHandler *rpc.Server // IPC RPC request handler to process the API requests ipcHandler *rpc.Server // IPC RPC request handler to process the API requests
httpHost string // HTTP hostname
httpPort int // HTTP post
httpEndpoint string // HTTP endpoint (interface + port) to listen at (empty = HTTP disabled) httpEndpoint string // HTTP endpoint (interface + port) to listen at (empty = HTTP disabled)
httpWhitelist []string // HTTP RPC modules to allow through this endpoint httpWhitelist []string // HTTP RPC modules to allow through this endpoint
httpCors string // HTTP RPC Cross-Origin Resource Sharing header httpCors string // HTTP RPC Cross-Origin Resource Sharing header
httpListener net.Listener // HTTP RPC listener socket to server API requests httpListener net.Listener // HTTP RPC listener socket to server API requests
httpHandler *rpc.Server // HTTP RPC request handler to process the API requests httpHandler *rpc.Server // HTTP RPC request handler to process the API requests
wsHost string // Websocket host
wsPort int // Websocket post
wsEndpoint string // Websocket endpoint (interface + port) to listen at (empty = websocket disabled) wsEndpoint string // Websocket endpoint (interface + port) to listen at (empty = websocket disabled)
wsWhitelist []string // Websocket RPC modules to allow through this endpoint wsWhitelist []string // Websocket RPC modules to allow through this endpoint
wsDomains string // Websocket RPC allowed origin domains wsOrigins string // Websocket RPC allowed origin domains
wsListener net.Listener // Websocket RPC listener socket to server API requests wsListener net.Listener // Websocket RPC listener socket to server API requests
wsHandler *rpc.Server // Websocket RPC request handler to process the API requests wsHandler *rpc.Server // Websocket RPC request handler to process the API requests
@ -110,12 +114,16 @@ func New(conf *Config) (*Node, error) {
}, },
serviceFuncs: []ServiceConstructor{}, serviceFuncs: []ServiceConstructor{},
ipcEndpoint: conf.IPCEndpoint(), ipcEndpoint: conf.IPCEndpoint(),
httpHost: conf.HTTPHost,
httpPort: conf.HTTPPort,
httpEndpoint: conf.HTTPEndpoint(), httpEndpoint: conf.HTTPEndpoint(),
httpWhitelist: conf.HTTPModules, httpWhitelist: conf.HTTPModules,
httpCors: conf.HTTPCors, httpCors: conf.HTTPCors,
wsHost: conf.WSHost,
wsPort: conf.WSPort,
wsEndpoint: conf.WSEndpoint(), wsEndpoint: conf.WSEndpoint(),
wsWhitelist: conf.WSModules, wsWhitelist: conf.WSModules,
wsDomains: conf.WSDomains, wsOrigins: conf.WSOrigins,
eventmux: new(event.TypeMux), eventmux: new(event.TypeMux),
}, nil }, nil
} }
@ -231,7 +239,7 @@ func (n *Node) startRPC(services map[reflect.Type]Service) error {
n.stopInProc() n.stopInProc()
return err return err
} }
if err := n.startWS(n.wsEndpoint, apis, n.wsWhitelist, n.wsDomains); err != nil { if err := n.startWS(n.wsEndpoint, apis, n.wsWhitelist, n.wsOrigins); err != nil {
n.stopHTTP() n.stopHTTP()
n.stopIPC() n.stopIPC()
n.stopInProc() n.stopInProc()
@ -383,7 +391,7 @@ func (n *Node) stopHTTP() {
} }
// startWS initializes and starts the websocket RPC endpoint. // startWS initializes and starts the websocket RPC endpoint.
func (n *Node) startWS(endpoint string, apis []rpc.API, modules []string, cors string) error { func (n *Node) startWS(endpoint string, apis []rpc.API, modules []string, wsOrigins string) error {
// Short circuit if the WS endpoint isn't being exposed // Short circuit if the WS endpoint isn't being exposed
if endpoint == "" { if endpoint == "" {
return nil return nil
@ -411,14 +419,14 @@ func (n *Node) startWS(endpoint string, apis []rpc.API, modules []string, cors s
if listener, err = net.Listen("tcp", endpoint); err != nil { if listener, err = net.Listen("tcp", endpoint); err != nil {
return err return err
} }
go rpc.NewWSServer(cors, handler).Serve(listener) go rpc.NewWSServer(wsOrigins, handler).Serve(listener)
glog.V(logger.Info).Infof("WebSocket endpoint opened: ws://%s", endpoint) glog.V(logger.Info).Infof("WebSocket endpoint opened: ws://%s", endpoint)
// All listeners booted successfully // All listeners booted successfully
n.wsEndpoint = endpoint n.wsEndpoint = endpoint
n.wsListener = listener n.wsListener = listener
n.wsHandler = handler n.wsHandler = handler
n.wsDomains = cors n.wsOrigins = wsOrigins
return nil return nil
} }

View File

@ -554,7 +554,7 @@ func TestAPIGather(t *testing.T) {
{"multi.v2.nested_theOneMethod", "multi.v2.nested"}, {"multi.v2.nested_theOneMethod", "multi.v2.nested"},
} }
for i, test := range tests { for i, test := range tests {
if err := client.Send(rpc.JSONRequest{Id: new(int64), Version: "2.0", Method: test.Method}); err != nil { if err := client.Send(rpc.JSONRequest{Id: []byte("1"), Version: "2.0", Method: test.Method}); err != nil {
t.Fatalf("test %d: failed to send API request: %v", i, err) t.Fatalf("test %d: failed to send API request: %v", i, err)
} }
reply := new(rpc.JSONSuccessResponse) reply := new(rpc.JSONSuccessResponse)

View File

@ -29,11 +29,23 @@ Methods that satisfy the following criteria are made available for remote access
- method returned value(s) must be exported or builtin types - method returned value(s) must be exported or builtin types
An example method: An example method:
func (s *CalcService) Div(a, b int) (int, error) func (s *CalcService) Add(a, b int) (int, error)
When the returned error isn't nil the returned integer is ignored and the error is When the returned error isn't nil the returned integer is ignored and the error is
send back to the client. Otherwise the returned integer is send back to the client. send back to the client. Otherwise the returned integer is send back to the client.
Optional arguments are supported by accepting pointer values as arguments. E.g.
if we want to do the addition in an optional finite field we can accept a mod
argument as pointer value.
func (s *CalService) Add(a, b int, mod *int) (int, error)
This RPC method can be called with 2 integers and a null value as third argument.
In that case the mod argument will be nil. Or it can be called with 3 integers,
in that case mod will be pointing to the given third argument. Since the optional
argument is the last argument the RPC package will also accept 2 integers as
arguments. It will pass the mod argument as nil to the RPC method.
The server offers the ServeCodec method which accepts a ServerCodec instance. It will The server offers the ServeCodec method which accepts a ServerCodec instance. It will
read requests from the codec, process the request and sends the response back to the read requests from the codec, process the request and sends the response back to the
client using the codec. The server can execute requests concurrently. Responses client using the codec. The server can execute requests concurrently. Responses

View File

@ -20,13 +20,12 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"io"
"github.com/rs/cors" "github.com/rs/cors"
) )
@ -37,6 +36,7 @@ const (
// httpClient connects to a geth RPC server over HTTP. // httpClient connects to a geth RPC server over HTTP.
type httpClient struct { type httpClient struct {
endpoint *url.URL // HTTP-RPC server endpoint endpoint *url.URL // HTTP-RPC server endpoint
httpClient http.Client // reuse connection
lastRes []byte // HTTP requests are synchronous, store last response lastRes []byte // HTTP requests are synchronous, store last response
} }
@ -57,30 +57,22 @@ func (client *httpClient) Send(msg interface{}) error {
var err error var err error
client.lastRes = nil client.lastRes = nil
if body, err = json.Marshal(msg); err != nil { if body, err = json.Marshal(msg); err != nil {
return err return err
} }
httpReq, err := http.NewRequest("POST", client.endpoint.String(), bytes.NewBuffer(body)) resp, err := client.httpClient.Post(client.endpoint.String(), "application/json", bytes.NewReader(body))
if err != nil { if err != nil {
return err return err
} }
httpReq.Header.Set("Content-Type", "application/json")
httpClient := http.Client{}
resp, err := httpClient.Do(httpReq)
if err != nil {
return err
}
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode == http.StatusOK { if resp.StatusCode == http.StatusOK {
client.lastRes, err = ioutil.ReadAll(resp.Body) client.lastRes, err = ioutil.ReadAll(resp.Body)
return err return err
} }
return fmt.Errorf("unable to handle request") return fmt.Errorf("request failed: %s", resp.Status)
} }
// Recv will try to deserialize the last received response into the given msg. // Recv will try to deserialize the last received response into the given msg.

View File

@ -22,7 +22,7 @@ import (
"net" "net"
"time" "time"
"github.com/microsoft/go-winio" winio "github.com/microsoft/go-winio"
) )
// ipcListen will create a named pipe on the given endpoint. // ipcListen will create a named pipe on the given endpoint.

View File

@ -19,7 +19,6 @@ package rpc
var ( var (
// Holds geth specific RPC extends which can be used to extend web3 // Holds geth specific RPC extends which can be used to extend web3
WEB3Extensions = map[string]string{ WEB3Extensions = map[string]string{
"personal": Personal_JS,
"txpool": TxPool_JS, "txpool": TxPool_JS,
"admin": Admin_JS, "admin": Admin_JS,
"eth": Eth_JS, "eth": Eth_JS,
@ -29,38 +28,6 @@ var (
} }
) )
const Personal_JS = `
web3._extend({
property: 'personal',
methods:
[
new web3._extend.Method({
name: 'newAccount',
call: 'personal_newAccount',
params: 1,
outputFormatter: web3._extend.utils.toAddress
}),
new web3._extend.Method({
name: 'unlockAccount',
call: 'personal_unlockAccount',
params: 3,
}),
new web3._extend.Method({
name: 'lockAccount',
call: 'personal_lockAccount',
params: 1
})
],
properties:
[
new web3._extend.Property({
name: 'listAccounts',
getter: 'personal_listAccounts'
})
]
});
`
const TxPool_JS = ` const TxPool_JS = `
web3._extend({ web3._extend({
property: 'txpool', property: 'txpool',
@ -124,22 +91,22 @@ web3._extend({
new web3._extend.Method({ new web3._extend.Method({
name: 'startRPC', name: 'startRPC',
call: 'admin_startRPC', call: 'admin_startRPC',
params: 4 params: 4,
inputFormatter: [null, null, null, null]
}), }),
new web3._extend.Method({ new web3._extend.Method({
name: 'stopRPC', name: 'stopRPC',
call: 'admin_stopRPC', call: 'admin_stopRPC'
params: 0
}), }),
new web3._extend.Method({ new web3._extend.Method({
name: 'startWS', name: 'startWS',
call: 'admin_startWS', call: 'admin_startWS',
params: 4 params: 4,
inputFormatter: [null, null, null, null]
}), }),
new web3._extend.Method({ new web3._extend.Method({
name: 'stopWS', name: 'stopWS',
call: 'admin_stopWS', call: 'admin_stopWS'
params: 0
}), }),
new web3._extend.Method({ new web3._extend.Method({
name: 'setGlobalRegistrar', name: 'setGlobalRegistrar',
@ -219,7 +186,7 @@ web3._extend({
name: 'sign', name: 'sign',
call: 'eth_sign', call: 'eth_sign',
params: 2, params: 2,
inputFormatter: [web3._extend.utils.toAddress, null] inputFormatter: [web3._extend.formatters.inputAddressFormatter, null]
}), }),
new web3._extend.Method({ new web3._extend.Method({
name: 'resend', name: 'resend',
@ -422,19 +389,18 @@ web3._extend({
new web3._extend.Method({ new web3._extend.Method({
name: 'start', name: 'start',
call: 'miner_start', call: 'miner_start',
params: 1 params: 1,
inputFormatter: [null]
}), }),
new web3._extend.Method({ new web3._extend.Method({
name: 'stop', name: 'stop',
call: 'miner_stop', call: 'miner_stop'
params: 1
}), }),
new web3._extend.Method({ new web3._extend.Method({
name: 'setEtherbase', name: 'setEtherbase',
call: 'miner_setEtherbase', call: 'miner_setEtherbase',
params: 1, params: 1,
inputFormatter: [web3._extend.formatters.formatInputInt], inputFormatter: [web3._extend.formatters.inputAddressFormatter]
outputFormatter: web3._extend.formatters.formatOutputBool
}), }),
new web3._extend.Method({ new web3._extend.Method({
name: 'setExtra', name: 'setExtra',

View File

@ -21,6 +21,7 @@ import (
"fmt" "fmt"
"io" "io"
"reflect" "reflect"
"strconv"
"strings" "strings"
"sync" "sync"
@ -40,14 +41,14 @@ const (
type JSONRequest struct { type JSONRequest struct {
Method string `json:"method"` Method string `json:"method"`
Version string `json:"jsonrpc"` Version string `json:"jsonrpc"`
Id *int64 `json:"id,omitempty"` Id json.RawMessage `json:"id,omitempty"`
Payload json.RawMessage `json:"params,omitempty"` Payload json.RawMessage `json:"params,omitempty"`
} }
// JSON-RPC response // JSON-RPC response
type JSONSuccessResponse struct { type JSONSuccessResponse struct {
Version string `json:"jsonrpc"` Version string `json:"jsonrpc"`
Id int64 `json:"id"` Id interface{} `json:"id,omitempty"`
Result interface{} `json:"result"` Result interface{} `json:"result"`
} }
@ -61,7 +62,7 @@ type JSONError struct {
// JSON-RPC error response // JSON-RPC error response
type JSONErrResponse struct { type JSONErrResponse struct {
Version string `json:"jsonrpc"` Version string `json:"jsonrpc"`
Id *int64 `json:"id,omitempty"` Id interface{} `json:"id,omitempty"`
Error JSONError `json:"error"` Error JSONError `json:"error"`
} }
@ -78,16 +79,16 @@ type jsonNotification struct {
Params jsonSubscription `json:"params"` Params jsonSubscription `json:"params"`
} }
// jsonCodec reads and writes JSON-RPC messages to the underlying connection. It also has support for parsing arguments // jsonCodec reads and writes JSON-RPC messages to the underlying connection. It
// and serializing (result) objects. // also has support for parsing arguments and serializing (result) objects.
type jsonCodec struct { type jsonCodec struct {
closed chan interface{} closer sync.Once // close closed channel once
closer sync.Once closed chan interface{} // closed on Close
d *json.Decoder decMu sync.Mutex // guards d
muEncoder sync.Mutex d *json.Decoder // decodes incoming requests
e *json.Encoder encMu sync.Mutex // guards e
req JSONRequest e *json.Encoder // encodes responses
rw io.ReadWriteCloser rw io.ReadWriteCloser // connection
} }
// NewJSONCodec creates a new RPC server codec with support for JSON-RPC 2.0 // NewJSONCodec creates a new RPC server codec with support for JSON-RPC 2.0
@ -109,9 +110,13 @@ func isBatch(msg json.RawMessage) bool {
return false return false
} }
// ReadRequestHeaders will read new requests without parsing the arguments. It will return a collection of requests, an // ReadRequestHeaders will read new requests without parsing the arguments. It will
// indication if these requests are in batch form or an error when the incoming message could not be read/parsed. // return a collection of requests, an indication if these requests are in batch
// form or an error when the incoming message could not be read/parsed.
func (c *jsonCodec) ReadRequestHeaders() ([]rpcRequest, bool, RPCError) { func (c *jsonCodec) ReadRequestHeaders() ([]rpcRequest, bool, RPCError) {
c.decMu.Lock()
defer c.decMu.Unlock()
var incomingMsg json.RawMessage var incomingMsg json.RawMessage
if err := c.d.Decode(&incomingMsg); err != nil { if err := c.d.Decode(&incomingMsg); err != nil {
return nil, false, &invalidRequestError{err.Error()} return nil, false, &invalidRequestError{err.Error()}
@ -124,21 +129,38 @@ func (c *jsonCodec) ReadRequestHeaders() ([]rpcRequest, bool, RPCError) {
return parseRequest(incomingMsg) return parseRequest(incomingMsg)
} }
// parseRequest will parse a single request from the given RawMessage. It will return the parsed request, an indication // checkReqId returns an error when the given reqId isn't valid for RPC method calls.
// if the request was a batch or an error when the request could not be parsed. // valid id's are strings, numbers or null
func checkReqId(reqId json.RawMessage) error {
if len(reqId) == 0 {
return fmt.Errorf("missing request id")
}
if _, err := strconv.ParseFloat(string(reqId), 64); err == nil {
return nil
}
var str string
if err := json.Unmarshal(reqId, &str); err == nil {
return nil
}
return fmt.Errorf("invalid request id")
}
// parseRequest will parse a single request from the given RawMessage. It will return
// the parsed request, an indication if the request was a batch or an error when
// the request could not be parsed.
func parseRequest(incomingMsg json.RawMessage) ([]rpcRequest, bool, RPCError) { func parseRequest(incomingMsg json.RawMessage) ([]rpcRequest, bool, RPCError) {
var in JSONRequest var in JSONRequest
if err := json.Unmarshal(incomingMsg, &in); err != nil { if err := json.Unmarshal(incomingMsg, &in); err != nil {
return nil, false, &invalidMessageError{err.Error()} return nil, false, &invalidMessageError{err.Error()}
} }
if in.Id == nil { if err := checkReqId(in.Id); err != nil {
return nil, false, &invalidMessageError{"Server cannot handle notifications"} return nil, false, &invalidMessageError{err.Error()}
} }
// subscribe are special, they will always use `subscribeMethod` as service method // subscribe are special, they will always use `subscribeMethod` as first param in the payload
if in.Method == subscribeMethod { if in.Method == subscribeMethod {
reqs := []rpcRequest{rpcRequest{id: *in.Id, isPubSub: true}} reqs := []rpcRequest{rpcRequest{id: &in.Id, isPubSub: true}}
if len(in.Payload) > 0 { if len(in.Payload) > 0 {
// first param must be subscription name // first param must be subscription name
var subscribeMethod [1]string var subscribeMethod [1]string
@ -156,7 +178,7 @@ func parseRequest(incomingMsg json.RawMessage) ([]rpcRequest, bool, RPCError) {
} }
if in.Method == unsubscribeMethod { if in.Method == unsubscribeMethod {
return []rpcRequest{rpcRequest{id: *in.Id, isPubSub: true, return []rpcRequest{rpcRequest{id: &in.Id, isPubSub: true,
method: unsubscribeMethod, params: in.Payload}}, false, nil method: unsubscribeMethod, params: in.Payload}}, false, nil
} }
@ -167,10 +189,10 @@ func parseRequest(incomingMsg json.RawMessage) ([]rpcRequest, bool, RPCError) {
} }
if len(in.Payload) == 0 { if len(in.Payload) == 0 {
return []rpcRequest{rpcRequest{service: elems[0], method: elems[1], id: *in.Id}}, false, nil return []rpcRequest{rpcRequest{service: elems[0], method: elems[1], id: &in.Id}}, false, nil
} }
return []rpcRequest{rpcRequest{service: elems[0], method: elems[1], id: *in.Id, params: in.Payload}}, false, nil return []rpcRequest{rpcRequest{service: elems[0], method: elems[1], id: &in.Id, params: in.Payload}}, false, nil
} }
// parseBatchRequest will parse a batch request into a collection of requests from the given RawMessage, an indication // parseBatchRequest will parse a batch request into a collection of requests from the given RawMessage, an indication
@ -183,14 +205,17 @@ func parseBatchRequest(incomingMsg json.RawMessage) ([]rpcRequest, bool, RPCErro
requests := make([]rpcRequest, len(in)) requests := make([]rpcRequest, len(in))
for i, r := range in { for i, r := range in {
if r.Id == nil { if err := checkReqId(r.Id); err != nil {
return nil, true, &invalidMessageError{"Server cannot handle notifications"} return nil, false, &invalidMessageError{err.Error()}
} }
// (un)subscribe are special, they will always use the same service.method id := &in[i].Id
// subscribe are special, they will always use `subscribeMethod` as first param in the payload
if r.Method == subscribeMethod { if r.Method == subscribeMethod {
requests[i] = rpcRequest{id: *r.Id, isPubSub: true} requests[i] = rpcRequest{id: id, isPubSub: true}
if len(r.Payload) > 0 { if len(r.Payload) > 0 {
// first param must be subscription name
var subscribeMethod [1]string var subscribeMethod [1]string
if err := json.Unmarshal(r.Payload, &subscribeMethod); err != nil { if err := json.Unmarshal(r.Payload, &subscribeMethod); err != nil {
glog.V(logger.Debug).Infof("Unable to parse subscription method: %v\n", err) glog.V(logger.Debug).Infof("Unable to parse subscription method: %v\n", err)
@ -207,7 +232,7 @@ func parseBatchRequest(incomingMsg json.RawMessage) ([]rpcRequest, bool, RPCErro
} }
if r.Method == unsubscribeMethod { if r.Method == unsubscribeMethod {
requests[i] = rpcRequest{id: *r.Id, isPubSub: true, method: unsubscribeMethod, params: r.Payload} requests[i] = rpcRequest{id: id, isPubSub: true, method: unsubscribeMethod, params: r.Payload}
continue continue
} }
@ -217,9 +242,9 @@ func parseBatchRequest(incomingMsg json.RawMessage) ([]rpcRequest, bool, RPCErro
} }
if len(r.Payload) == 0 { if len(r.Payload) == 0 {
requests[i] = rpcRequest{service: elems[0], method: elems[1], id: *r.Id, params: nil} requests[i] = rpcRequest{service: elems[0], method: elems[1], id: id, params: nil}
} else { } else {
requests[i] = rpcRequest{service: elems[0], method: elems[1], id: *r.Id, params: r.Payload} requests[i] = rpcRequest{service: elems[0], method: elems[1], id: id, params: r.Payload}
} }
} }
@ -236,58 +261,38 @@ func (c *jsonCodec) ParseRequestArguments(argTypes []reflect.Type, params interf
} }
} }
func countArguments(args json.RawMessage) (int, error) { // parsePositionalArguments tries to parse the given args to an array of values with the given types.
var cnt []interface{} // It returns the parsed values or an error when the args could not be parsed. Missing optional arguments
if err := json.Unmarshal(args, &cnt); err != nil { // are returned as reflect.Zero values.
return -1, nil func parsePositionalArguments(args json.RawMessage, callbackArgs []reflect.Type) ([]reflect.Value, RPCError) {
} params := make([]interface{}, 0, len(callbackArgs))
return len(cnt), nil for _, t := range callbackArgs {
} params = append(params, reflect.New(t).Interface())
// parsePositionalArguments tries to parse the given args to an array of values with the given types. It returns the
// parsed values or an error when the args could not be parsed.
func parsePositionalArguments(args json.RawMessage, argTypes []reflect.Type) ([]reflect.Value, RPCError) {
argValues := make([]reflect.Value, len(argTypes))
params := make([]interface{}, len(argTypes))
n, err := countArguments(args)
if err != nil {
return nil, &invalidParamsError{err.Error()}
}
if n != len(argTypes) {
return nil, &invalidParamsError{fmt.Sprintf("insufficient params, want %d have %d", len(argTypes), n)}
}
for i, t := range argTypes {
if t.Kind() == reflect.Ptr {
// values must be pointers for the Unmarshal method, reflect.
// Dereference otherwise reflect.New would create **SomeType
argValues[i] = reflect.New(t.Elem())
params[i] = argValues[i].Interface()
// when not specified blockNumbers are by default latest (-1)
if blockNumber, ok := params[i].(*BlockNumber); ok {
*blockNumber = BlockNumber(-1)
}
} else {
argValues[i] = reflect.New(t)
params[i] = argValues[i].Interface()
// when not specified blockNumbers are by default latest (-1)
if blockNumber, ok := params[i].(*BlockNumber); ok {
*blockNumber = BlockNumber(-1)
}
}
} }
if err := json.Unmarshal(args, &params); err != nil { if err := json.Unmarshal(args, &params); err != nil {
return nil, &invalidParamsError{err.Error()} return nil, &invalidParamsError{err.Error()}
} }
// Convert pointers back to values where necessary if len(params) > len(callbackArgs) {
for i, a := range argValues { return nil, &invalidParamsError{fmt.Sprintf("too many params, want %d got %d", len(callbackArgs), len(params))}
if a.Kind() != argTypes[i].Kind() { }
argValues[i] = reflect.Indirect(argValues[i])
// assume missing params are null values
for i := len(params); i < len(callbackArgs); i++ {
params = append(params, nil)
}
argValues := make([]reflect.Value, len(params))
for i, p := range params {
// verify that JSON null values are only supplied for optional arguments (ptr types)
if p == nil && callbackArgs[i].Kind() != reflect.Ptr {
return nil, &invalidParamsError{fmt.Sprintf("invalid or missing value for params[%d]", i)}
}
if p == nil {
argValues[i] = reflect.Zero(callbackArgs[i])
} else { // deref pointers values creates previously with reflect.New
argValues[i] = reflect.ValueOf(p).Elem()
} }
} }
@ -295,7 +300,7 @@ func parsePositionalArguments(args json.RawMessage, argTypes []reflect.Type) ([]
} }
// CreateResponse will create a JSON-RPC success response with the given id and reply as result. // CreateResponse will create a JSON-RPC success response with the given id and reply as result.
func (c *jsonCodec) CreateResponse(id int64, reply interface{}) interface{} { func (c *jsonCodec) CreateResponse(id interface{}, reply interface{}) interface{} {
if isHexNum(reflect.TypeOf(reply)) { if isHexNum(reflect.TypeOf(reply)) {
return &JSONSuccessResponse{Version: jsonRPCVersion, Id: id, Result: fmt.Sprintf(`%#x`, reply)} return &JSONSuccessResponse{Version: jsonRPCVersion, Id: id, Result: fmt.Sprintf(`%#x`, reply)}
} }
@ -303,13 +308,13 @@ func (c *jsonCodec) CreateResponse(id int64, reply interface{}) interface{} {
} }
// CreateErrorResponse will create a JSON-RPC error response with the given id and error. // CreateErrorResponse will create a JSON-RPC error response with the given id and error.
func (c *jsonCodec) CreateErrorResponse(id *int64, err RPCError) interface{} { func (c *jsonCodec) CreateErrorResponse(id interface{}, err RPCError) interface{} {
return &JSONErrResponse{Version: jsonRPCVersion, Id: id, Error: JSONError{Code: err.Code(), Message: err.Error()}} return &JSONErrResponse{Version: jsonRPCVersion, Id: id, Error: JSONError{Code: err.Code(), Message: err.Error()}}
} }
// CreateErrorResponseWithInfo will create a JSON-RPC error response with the given id and error. // CreateErrorResponseWithInfo will create a JSON-RPC error response with the given id and error.
// info is optional and contains additional information about the error. When an empty string is passed it is ignored. // info is optional and contains additional information about the error. When an empty string is passed it is ignored.
func (c *jsonCodec) CreateErrorResponseWithInfo(id *int64, err RPCError, info interface{}) interface{} { func (c *jsonCodec) CreateErrorResponseWithInfo(id interface{}, err RPCError, info interface{}) interface{} {
return &JSONErrResponse{Version: jsonRPCVersion, Id: id, return &JSONErrResponse{Version: jsonRPCVersion, Id: id,
Error: JSONError{Code: err.Code(), Message: err.Error(), Data: info}} Error: JSONError{Code: err.Code(), Message: err.Error(), Data: info}}
} }
@ -327,8 +332,8 @@ func (c *jsonCodec) CreateNotification(subid string, event interface{}) interfac
// Write message to client // Write message to client
func (c *jsonCodec) Write(res interface{}) error { func (c *jsonCodec) Write(res interface{}) error {
c.muEncoder.Lock() c.encMu.Lock()
defer c.muEncoder.Unlock() defer c.encMu.Unlock()
return c.e.Encode(res) return c.e.Encode(res)
} }

View File

@ -3,7 +3,9 @@ package rpc
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"encoding/json"
"reflect" "reflect"
"strconv"
"testing" "testing"
) )
@ -51,8 +53,16 @@ func TestJSONRequestParsing(t *testing.T) {
t.Fatalf("Expected method 'Add' but got '%s'", requests[0].method) t.Fatalf("Expected method 'Add' but got '%s'", requests[0].method)
} }
if requests[0].id != 1234 { if rawId, ok := requests[0].id.(*json.RawMessage); ok {
t.Fatalf("Expected id 1234 but got %d", requests[0].id) id, e := strconv.ParseInt(string(*rawId), 0, 64)
if e != nil {
t.Fatalf("%v", e)
}
if id != 1234 {
t.Fatalf("Expected id 1234 but got %s", id)
}
} else {
t.Fatalf("invalid request, expected *json.RawMesage got %T", requests[0].id)
} }
var arg int var arg int
@ -71,3 +81,82 @@ func TestJSONRequestParsing(t *testing.T) {
t.Fatalf("expected %d == 11 && %d == 22", v[0].Int(), v[1].Int()) t.Fatalf("expected %d == 11 && %d == 22", v[0].Int(), v[1].Int())
} }
} }
func TestJSONRequestParamsParsing(t *testing.T) {
var (
stringT = reflect.TypeOf("")
intT = reflect.TypeOf(0)
intPtrT = reflect.TypeOf(new(int))
stringV = reflect.ValueOf("abc")
i = 1
intV = reflect.ValueOf(i)
intPtrV = reflect.ValueOf(&i)
)
var validTests = []struct {
input string
argTypes []reflect.Type
expected []reflect.Value
}{
{`[]`, []reflect.Type{}, []reflect.Value{}},
{`[]`, []reflect.Type{intPtrT}, []reflect.Value{intPtrV}},
{`[1]`, []reflect.Type{intT}, []reflect.Value{intV}},
{`[1,"abc"]`, []reflect.Type{intT, stringT}, []reflect.Value{intV, stringV}},
{`[null]`, []reflect.Type{intPtrT}, []reflect.Value{intPtrV}},
{`[null,"abc"]`, []reflect.Type{intPtrT, stringT, intPtrT}, []reflect.Value{intPtrV, stringV, intPtrV}},
{`[null,"abc",null]`, []reflect.Type{intPtrT, stringT, intPtrT}, []reflect.Value{intPtrV, stringV, intPtrV}},
}
codec := jsonCodec{}
for _, test := range validTests {
params := (json.RawMessage)([]byte(test.input))
args, err := codec.ParseRequestArguments(test.argTypes, params)
if err != nil {
t.Fatal(err)
}
var match []interface{}
json.Unmarshal([]byte(test.input), &match)
if len(args) != len(test.argTypes) {
t.Fatalf("expected %d parsed args, got %d", len(test.argTypes), len(args))
}
for i, arg := range args {
expected := test.expected[i]
if arg.Kind() != expected.Kind() {
t.Errorf("expected type for param %d in %s", i, test.input)
}
if arg.Kind() == reflect.Int && arg.Int() != expected.Int() {
t.Errorf("expected int(%d), got int(%d) in %s", expected.Int(), arg.Int(), test.input)
}
if arg.Kind() == reflect.String && arg.String() != expected.String() {
t.Errorf("expected string(%s), got string(%s) in %s", expected.String(), arg.String(), test.input)
}
}
}
var invalidTests = []struct {
input string
argTypes []reflect.Type
}{
{`[]`, []reflect.Type{intT}},
{`[null]`, []reflect.Type{intT}},
{`[1]`, []reflect.Type{stringT}},
{`[1,2]`, []reflect.Type{stringT}},
{`["abc", null]`, []reflect.Type{stringT, intT}},
}
for i, test := range invalidTests {
if _, err := codec.ParseRequestArguments(test.argTypes, test.input); err == nil {
t.Errorf("expected test %d - %s to fail", i, test.input)
}
}
}

View File

@ -18,10 +18,9 @@ package rpc
import ( import (
"encoding/json" "encoding/json"
"fmt" "net"
"reflect" "reflect"
"testing" "testing"
"time"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
@ -69,10 +68,6 @@ func (s *Service) Subscription(ctx context.Context) (Subscription, error) {
return nil, nil return nil, nil
} }
func (s *Service) SubsriptionWithArgs(ctx context.Context, a, b int) (Subscription, error) {
return nil, nil
}
func TestServerRegisterName(t *testing.T) { func TestServerRegisterName(t *testing.T) {
server := NewServer() server := NewServer()
service := new(Service) service := new(Service)
@ -94,182 +89,67 @@ func TestServerRegisterName(t *testing.T) {
t.Errorf("Expected 4 callbacks for service 'calc', got %d", len(svc.callbacks)) t.Errorf("Expected 4 callbacks for service 'calc', got %d", len(svc.callbacks))
} }
if len(svc.subscriptions) != 2 { if len(svc.subscriptions) != 1 {
t.Errorf("Expected 2 subscriptions for service 'calc', got %d", len(svc.subscriptions)) t.Errorf("Expected 1 subscription for service 'calc', got %d", len(svc.subscriptions))
} }
} }
// dummy codec used for testing RPC method execution func testServerMethodExecution(t *testing.T, method string) {
type ServerTestCodec struct { server := NewServer()
counter int service := new(Service)
input []byte
output string
closer chan interface{}
}
func (c *ServerTestCodec) ReadRequestHeaders() ([]rpcRequest, bool, RPCError) { if err := server.RegisterName("test", service); err != nil {
c.counter += 1 t.Fatalf("%v", err)
if c.counter == 1 {
var req JSONRequest
json.Unmarshal(c.input, &req)
return []rpcRequest{rpcRequest{id: *req.Id, isPubSub: false, service: "test", method: req.Method, params: req.Payload}}, false, nil
} }
// requests are executes in parallel, wait a bit before returning an error so that the previous request has time to stringArg := "string arg"
// be executed intArg := 1122
timer := time.NewTimer(time.Duration(2) * time.Second) argsArg := &Args{"abcde"}
<-timer.C params := []interface{}{stringArg, intArg, argsArg}
return nil, false, &invalidRequestError{"connection closed"}
}
func (c *ServerTestCodec) ParseRequestArguments(argTypes []reflect.Type, payload interface{}) ([]reflect.Value, RPCError) {
args, _ := payload.(json.RawMessage)
argValues := make([]reflect.Value, len(argTypes))
params := make([]interface{}, len(argTypes))
n, err := countArguments(args)
if err != nil {
return nil, &invalidParamsError{err.Error()}
}
if n != len(argTypes) {
return nil, &invalidParamsError{fmt.Sprintf("insufficient params, want %d have %d", len(argTypes), n)}
request := map[string]interface{}{
"id": 12345,
"method": "test_" + method,
"version": "2.0",
"params": params,
} }
for i, t := range argTypes { clientConn, serverConn := net.Pipe()
if t.Kind() == reflect.Ptr { defer clientConn.Close()
// values must be pointers for the Unmarshal method, reflect.
// Dereference otherwise reflect.New would create **SomeType
argValues[i] = reflect.New(t.Elem())
params[i] = argValues[i].Interface()
// when not specified blockNumbers are by default latest (-1) go server.ServeCodec(NewJSONCodec(serverConn), OptionMethodInvocation)
if blockNumber, ok := params[i].(*BlockNumber); ok {
*blockNumber = BlockNumber(-1) out := json.NewEncoder(clientConn)
in := json.NewDecoder(clientConn)
if err := out.Encode(request); err != nil {
t.Fatal(err)
}
response := JSONSuccessResponse{Result: &Result{}}
if err := in.Decode(&response); err != nil {
t.Fatal(err)
}
if result, ok := response.Result.(*Result); ok {
if result.String != stringArg {
t.Errorf("expected %s, got : %s\n", stringArg, result.String)
}
if result.Int != intArg {
t.Errorf("expected %d, got %d\n", intArg, result.Int)
}
if !reflect.DeepEqual(result.Args, argsArg) {
t.Errorf("expected %v, got %v\n", argsArg, result)
} }
} else { } else {
argValues[i] = reflect.New(t) t.Fatalf("invalid response: expected *Result - got: %T", response.Result)
params[i] = argValues[i].Interface()
// when not specified blockNumbers are by default latest (-1)
if blockNumber, ok := params[i].(*BlockNumber); ok {
*blockNumber = BlockNumber(-1)
} }
}
}
if err := json.Unmarshal(args, &params); err != nil {
return nil, &invalidParamsError{err.Error()}
}
// Convert pointers back to values where necessary
for i, a := range argValues {
if a.Kind() != argTypes[i].Kind() {
argValues[i] = reflect.Indirect(argValues[i])
}
}
return argValues, nil
}
func (c *ServerTestCodec) CreateResponse(id int64, reply interface{}) interface{} {
return &JSONSuccessResponse{Version: jsonRPCVersion, Id: id, Result: reply}
}
func (c *ServerTestCodec) CreateErrorResponse(id *int64, err RPCError) interface{} {
return &JSONErrResponse{Version: jsonRPCVersion, Id: id, Error: JSONError{Code: err.Code(), Message: err.Error()}}
}
func (c *ServerTestCodec) CreateErrorResponseWithInfo(id *int64, err RPCError, info interface{}) interface{} {
return &JSONErrResponse{Version: jsonRPCVersion, Id: id,
Error: JSONError{Code: err.Code(), Message: err.Error(), Data: info}}
}
func (c *ServerTestCodec) CreateNotification(subid string, event interface{}) interface{} {
return &jsonNotification{Version: jsonRPCVersion, Method: notificationMethod,
Params: jsonSubscription{Subscription: subid, Result: event}}
}
func (c *ServerTestCodec) Write(msg interface{}) error {
if len(c.output) == 0 { // only capture first response
if o, err := json.Marshal(msg); err != nil {
return err
} else {
c.output = string(o)
}
}
return nil
}
func (c *ServerTestCodec) Close() {
close(c.closer)
}
func (c *ServerTestCodec) Closed() <-chan interface{} {
return c.closer
} }
func TestServerMethodExecution(t *testing.T) { func TestServerMethodExecution(t *testing.T) {
server := NewServer() testServerMethodExecution(t, "echo")
service := new(Service)
if err := server.RegisterName("test", service); err != nil {
t.Fatalf("%v", err)
}
id := int64(12345)
req := JSONRequest{
Method: "echo",
Version: "2.0",
Id: &id,
}
args := []interface{}{"string arg", 1122, &Args{"qwerty"}}
req.Payload, _ = json.Marshal(&args)
input, _ := json.Marshal(&req)
codec := &ServerTestCodec{input: input, closer: make(chan interface{})}
go server.ServeCodec(codec, OptionMethodInvocation)
<-codec.closer
expected := `{"jsonrpc":"2.0","id":12345,"result":{"String":"string arg","Int":1122,"Args":{"S":"qwerty"}}}`
if expected != codec.output {
t.Fatalf("expected %s, got %s\n", expected, codec.output)
}
} }
func TestServerMethodWithCtx(t *testing.T) { func TestServerMethodWithCtx(t *testing.T) {
server := NewServer() testServerMethodExecution(t, "echoWithCtx")
service := new(Service)
if err := server.RegisterName("test", service); err != nil {
t.Fatalf("%v", err)
}
id := int64(12345)
req := JSONRequest{
Method: "echoWithCtx",
Version: "2.0",
Id: &id,
}
args := []interface{}{"string arg", 1122, &Args{"qwerty"}}
req.Payload, _ = json.Marshal(&args)
input, _ := json.Marshal(&req)
codec := &ServerTestCodec{input: input, closer: make(chan interface{})}
go server.ServeCodec(codec, OptionMethodInvocation)
<-codec.closer
expected := `{"jsonrpc":"2.0","id":12345,"result":{"String":"string arg","Int":1122,"Args":{"S":"qwerty"}}}`
if expected != codec.output {
t.Fatalf("expected %s, got %s\n", expected, codec.output)
}
} }

View File

@ -56,7 +56,7 @@ type service struct {
// serverRequest is an incoming request // serverRequest is an incoming request
type serverRequest struct { type serverRequest struct {
id int64 id interface{}
svcname string svcname string
rcvr reflect.Value rcvr reflect.Value
callb *callback callb *callback
@ -85,7 +85,7 @@ type Server struct {
type rpcRequest struct { type rpcRequest struct {
service string service string
method string method string
id int64 id interface{}
isPubSub bool isPubSub bool
params interface{} params interface{}
} }
@ -106,12 +106,12 @@ type ServerCodec interface {
ReadRequestHeaders() ([]rpcRequest, bool, RPCError) ReadRequestHeaders() ([]rpcRequest, bool, RPCError)
// Parse request argument to the given types // Parse request argument to the given types
ParseRequestArguments([]reflect.Type, interface{}) ([]reflect.Value, RPCError) ParseRequestArguments([]reflect.Type, interface{}) ([]reflect.Value, RPCError)
// Assemble success response // Assemble success response, expects response id and payload
CreateResponse(int64, interface{}) interface{} CreateResponse(interface{}, interface{}) interface{}
// Assemble error response // Assemble error response, expects response id and error
CreateErrorResponse(*int64, RPCError) interface{} CreateErrorResponse(interface{}, RPCError) interface{}
// Assemble error response with extra information about the error through info // Assemble error response with extra information about the error through info
CreateErrorResponseWithInfo(id *int64, err RPCError, info interface{}) interface{} CreateErrorResponseWithInfo(id interface{}, err RPCError, info interface{}) interface{}
// Create notification response // Create notification response
CreateNotification(string, interface{}) interface{} CreateNotification(string, interface{}) interface{}
// Write msg to client. // Write msg to client.
@ -207,43 +207,6 @@ func (h *HexNumber) BigInt() *big.Int {
return (*big.Int)(h) return (*big.Int)(h)
} }
type Number int64
func (n *Number) UnmarshalJSON(data []byte) error {
input := strings.TrimSpace(string(data))
if len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' {
input = input[1 : len(input)-1]
}
if len(input) == 0 {
*n = Number(latestBlockNumber.Int64())
return nil
}
in := new(big.Int)
_, ok := in.SetString(input, 0)
if !ok { // test if user supplied string tag
return fmt.Errorf(`invalid number %s`, data)
}
if in.Cmp(earliestBlockNumber) >= 0 && in.Cmp(maxBlockNumber) <= 0 {
*n = Number(in.Int64())
return nil
}
return fmt.Errorf("blocknumber not in range [%d, %d]", earliestBlockNumber, maxBlockNumber)
}
func (n *Number) Int64() int64 {
return *(*int64)(n)
}
func (n *Number) BigInt() *big.Int {
return big.NewInt(n.Int64())
}
var ( var (
pendingBlockNumber = big.NewInt(-2) pendingBlockNumber = big.NewInt(-2)
latestBlockNumber = big.NewInt(-1) latestBlockNumber = big.NewInt(-1)

View File

@ -232,7 +232,7 @@ func newSubscriptionID() (string, error) {
// on which the given client connects. // on which the given client connects.
func SupportedModules(client Client) (map[string]string, error) { func SupportedModules(client Client) (map[string]string, error) {
req := JSONRequest{ req := JSONRequest{
Id: new(int64), Id: []byte("1"),
Version: "2.0", Version: "2.0",
Method: "rpc_modules", Method: "rpc_modules",
} }

View File

@ -88,10 +88,10 @@ func wsHandshakeValidator(allowedOrigins []string) func(*websocket.Config, *http
} }
// NewWSServer creates a new websocket RPC server around an API provider. // NewWSServer creates a new websocket RPC server around an API provider.
func NewWSServer(cors string, handler *Server) *http.Server { func NewWSServer(allowedOrigins string, handler *Server) *http.Server {
return &http.Server{ return &http.Server{
Handler: websocket.Server{ Handler: websocket.Server{
Handshake: wsHandshakeValidator(strings.Split(cors, ",")), Handshake: wsHandshakeValidator(strings.Split(allowedOrigins, ",")),
Handler: func(conn *websocket.Conn) { Handler: func(conn *websocket.Conn) {
handler.ServeCodec(NewJSONCodec(&wsReaderWriterCloser{conn}), handler.ServeCodec(NewJSONCodec(&wsReaderWriterCloser{conn}),
OptionMethodInvocation|OptionSubscriptions) OptionMethodInvocation|OptionSubscriptions)

View File

@ -84,7 +84,7 @@ type NewFilterArgs struct {
} }
// NewWhisperFilter creates and registers a new message filter to watch for inbound whisper messages. // NewWhisperFilter creates and registers a new message filter to watch for inbound whisper messages.
func (s *PublicWhisperAPI) NewFilter(args *NewFilterArgs) (*rpc.HexNumber, error) { func (s *PublicWhisperAPI) NewFilter(args NewFilterArgs) (*rpc.HexNumber, error) {
if s.w == nil { if s.w == nil {
return nil, whisperOffLineErr return nil, whisperOffLineErr
} }
@ -171,7 +171,7 @@ type PostArgs struct {
} }
// Post injects a message into the whisper network for distribution. // Post injects a message into the whisper network for distribution.
func (s *PublicWhisperAPI) Post(args *PostArgs) (bool, error) { func (s *PublicWhisperAPI) Post(args PostArgs) (bool, error) {
if s.w == nil { if s.w == nil {
return false, whisperOffLineErr return false, whisperOffLineErr
} }