mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-18 09:53:48 +00:00
cmd/signer: Made lowercase json-definitions, added UI-signer test functionality
This commit is contained in:
parent
d0c0b9bacd
commit
cd84631194
5 changed files with 187 additions and 61 deletions
|
|
@ -45,70 +45,73 @@ type SignerAPI struct {
|
||||||
|
|
||||||
// Metadata about the request
|
// Metadata about the request
|
||||||
type Metadata struct {
|
type Metadata struct {
|
||||||
remote string
|
Remote string `json:"remote"`
|
||||||
local string
|
Local string `json:"local"`
|
||||||
scheme string
|
Scheme string `json:"scheme"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// types for the requests/response types
|
// types for the requests/response types
|
||||||
type (
|
type (
|
||||||
// SignTxRequest contains info about a Transaction to sign
|
// SignTxRequest contains info about a Transaction to sign
|
||||||
SignTxRequest struct {
|
SignTxRequest struct {
|
||||||
Transaction TransactionArg
|
Transaction TransactionArg `json:"transaction"`
|
||||||
From common.Address
|
From common.Address `json:"fromaccount"`
|
||||||
Callinfo fmt.Stringer
|
Callinfo fmt.Stringer `json:"call_info"`
|
||||||
Meta Metadata
|
Meta Metadata `json:"meta"`
|
||||||
}
|
}
|
||||||
// SignTxResponse result from SignTxRequest
|
// SignTxResponse result from SignTxRequest
|
||||||
SignTxResponse struct {
|
SignTxResponse struct {
|
||||||
//The UI may make changes to the TX
|
//The UI may make changes to the TX
|
||||||
Transaction TransactionArg
|
Transaction TransactionArg
|
||||||
From common.Address
|
From common.Address
|
||||||
Approved bool
|
Approved bool `json:"approved"`
|
||||||
Password string
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
// ExportRequest info about query to export accounts
|
// ExportRequest info about query to export accounts
|
||||||
ExportRequest struct {
|
ExportRequest struct {
|
||||||
Address common.Address
|
Address common.Address `json:"address"`
|
||||||
Meta Metadata
|
Meta Metadata `json:"meta"`
|
||||||
}
|
}
|
||||||
// ExportResponse response to export-request
|
// ExportResponse response to export-request
|
||||||
ExportResponse struct {
|
ExportResponse struct {
|
||||||
Approved bool
|
Approved bool `json:"approved"`
|
||||||
}
|
}
|
||||||
// ImportRequest info about request to import an Account
|
// ImportRequest info about request to import an Account
|
||||||
ImportRequest struct {
|
ImportRequest struct {
|
||||||
Meta Metadata
|
Meta Metadata `json:"meta"`
|
||||||
}
|
}
|
||||||
ImportResponse struct {
|
ImportResponse struct {
|
||||||
Approved bool
|
Approved bool `json:"approved"`
|
||||||
OldPassword string
|
OldPassword string `json:"old_password"`
|
||||||
NewPassword string
|
NewPassword string `json:"new_password"`
|
||||||
}
|
}
|
||||||
SignDataRequest struct {
|
SignDataRequest struct {
|
||||||
Address common.Address
|
Address common.Address `json:"address"`
|
||||||
Rawdata hexutil.Bytes
|
Rawdata hexutil.Bytes `json:"raw_data"`
|
||||||
Message string
|
Message string `json:"message"`
|
||||||
Hash hexutil.Bytes
|
Hash hexutil.Bytes `json:"hash"`
|
||||||
Meta Metadata
|
Meta Metadata `json:"meta"`
|
||||||
}
|
}
|
||||||
SignDataResponse struct {
|
SignDataResponse struct {
|
||||||
Approved bool
|
Approved bool `json:"approved"`
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
NewAccountRequest struct {
|
NewAccountRequest struct {
|
||||||
Meta Metadata
|
Meta Metadata `json:"meta"`
|
||||||
}
|
}
|
||||||
NewAccountResponse struct {
|
NewAccountResponse struct {
|
||||||
Approved bool
|
Approved bool `json:"approved"`
|
||||||
Password string
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
ListRequest struct {
|
ListRequest struct {
|
||||||
Accounts []Account
|
Accounts []Account `json:"accounts"`
|
||||||
Meta Metadata
|
Meta Metadata `json:"meta"`
|
||||||
}
|
}
|
||||||
ListResponse struct {
|
ListResponse struct {
|
||||||
Accounts []Account
|
Accounts []Account `json:"accounts"`
|
||||||
|
}
|
||||||
|
Message struct {
|
||||||
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -186,13 +189,13 @@ func metaData(ctx context.Context) Metadata {
|
||||||
m := Metadata{"NA", "NA", "NA"}
|
m := Metadata{"NA", "NA", "NA"}
|
||||||
|
|
||||||
if v := ctx.Value("remote"); v != nil {
|
if v := ctx.Value("remote"); v != nil {
|
||||||
m.remote = v.(string)
|
m.Remote = v.(string)
|
||||||
}
|
}
|
||||||
if v := ctx.Value("scheme"); v != nil {
|
if v := ctx.Value("scheme"); v != nil {
|
||||||
m.scheme = v.(string)
|
m.Scheme = v.(string)
|
||||||
}
|
}
|
||||||
if v := ctx.Value("local"); v != nil {
|
if v := ctx.Value("local"); v != nil {
|
||||||
m.local = v.(string)
|
m.Local = v.(string)
|
||||||
}
|
}
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
@ -463,7 +466,7 @@ func (api *SignerAPI) Export(ctx context.Context, addr common.Address) (json.Raw
|
||||||
// Imports tries to import the given keyJSON in the local keystore. The keyJSON data is expected to be
|
// Imports tries to import the given keyJSON in the local keystore. The keyJSON data is expected to be
|
||||||
// in web3 keystore format. It will decrypt the keyJSON with the given passphrase and on successful
|
// in web3 keystore format. It will decrypt the keyJSON with the given passphrase and on successful
|
||||||
// decryption it will encrypt the key with the given newPassphrase and store it in the keystore.
|
// decryption it will encrypt the key with the given newPassphrase and store it in the keystore.
|
||||||
func (api *SignerAPI) Import(ctx context.Context, string, keyJSON json.RawMessage) (Account, error) {
|
func (api *SignerAPI) Import(ctx context.Context, keyJSON json.RawMessage) (Account, error) {
|
||||||
|
|
||||||
be := api.am.Backends(keystore.KeyStoreType)
|
be := api.am.Backends(keystore.KeyStoreType)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ func (ui *CommandlineUI) confirm() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func showMetadata(metadata Metadata) {
|
func showMetadata(metadata Metadata) {
|
||||||
fmt.Printf("Request info:\n\t%v -> %v -> %v\n", metadata.remote, metadata.scheme, metadata.local)
|
fmt.Printf("Request info:\n\t%v -> %v -> %v\n", metadata.Remote, metadata.Scheme, metadata.Local)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApproveTx prompt the user for confirmation to request to sign Transaction
|
// ApproveTx prompt the user for confirmation to request to sign Transaction
|
||||||
|
|
@ -100,7 +100,7 @@ func (ui *CommandlineUI) ApproveTx(request *SignTxRequest) (SignTxResponse, erro
|
||||||
weival := request.Transaction.Value
|
weival := request.Transaction.Value
|
||||||
|
|
||||||
fmt.Printf("--------- Transaction request-------------\n")
|
fmt.Printf("--------- Transaction request-------------\n")
|
||||||
fmt.Printf("to: %v\n", request.Transaction.To.Hex())
|
fmt.Printf("to: %v\n", request.Transaction.To)
|
||||||
fmt.Printf("from: %v\n", request.From.Hex())
|
fmt.Printf("from: %v\n", request.From.Hex())
|
||||||
fmt.Printf("value: %v wei\n", weival)
|
fmt.Printf("value: %v wei\n", weival)
|
||||||
if len(request.Transaction.Data) > 0 {
|
if len(request.Transaction.Data) > 0 {
|
||||||
|
|
@ -125,7 +125,7 @@ func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResp
|
||||||
|
|
||||||
fmt.Printf("-------- Sign data request--------------\n")
|
fmt.Printf("-------- Sign data request--------------\n")
|
||||||
fmt.Printf("Account: %x\n", request.Address)
|
fmt.Printf("Account: %x\n", request.Address)
|
||||||
fmt.Printf("message: \n%v\n", request.Message)
|
fmt.Printf("message: \n%q\n", request.Message)
|
||||||
fmt.Printf("raw data: \n%v\n", request.Rawdata)
|
fmt.Printf("raw data: \n%v\n", request.Rawdata)
|
||||||
fmt.Printf("message hash: %v\n", request.Hash)
|
fmt.Printf("message hash: %v\n", request.Hash)
|
||||||
fmt.Printf("-------------------------------------------\n")
|
fmt.Printf("-------------------------------------------\n")
|
||||||
|
|
@ -154,7 +154,7 @@ func (ui *CommandlineUI) ApproveImport(request *ImportRequest) (ImportResponse,
|
||||||
ui.mu.Lock()
|
ui.mu.Lock()
|
||||||
defer ui.mu.Unlock()
|
defer ui.mu.Unlock()
|
||||||
|
|
||||||
fmt.Printf("-------- Export Account request--------------\n")
|
fmt.Printf("-------- Import Account request--------------\n")
|
||||||
fmt.Printf("A request has been made to import an encrypted keyfile\n")
|
fmt.Printf("A request has been made to import an encrypted keyfile\n")
|
||||||
fmt.Printf("-------------------------------------------\n")
|
fmt.Printf("-------------------------------------------\n")
|
||||||
showMetadata(request.Meta)
|
showMetadata(request.Meta)
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,10 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/node"
|
"github.com/ethereum/go-ethereum/node"
|
||||||
"github.com/ethereum/go-ethereum/params"
|
"github.com/ethereum/go-ethereum/params"
|
||||||
|
|
@ -84,6 +87,10 @@ func main() {
|
||||||
"This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user " +
|
"This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user " +
|
||||||
"interface, and can be used when the signer is started by an external process.",
|
"interface, and can be used when the signer is started by an external process.",
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "stdio-ui-test",
|
||||||
|
Usage: "Mechanism to test interface between signer and UI. Requires 'stdio-ui'.",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Action = func(c *cli.Context) error {
|
app.Action = func(c *cli.Context) error {
|
||||||
|
|
@ -136,6 +143,7 @@ func main() {
|
||||||
if err = server.RegisterName("account", api); err != nil {
|
if err = server.RegisterName("account", api); err != nil {
|
||||||
utils.Fatalf("Could not register signer API: %v", err)
|
utils.Fatalf("Could not register signer API: %v", err)
|
||||||
}
|
}
|
||||||
|
//server.ListServices()
|
||||||
|
|
||||||
// Import from file
|
// Import from file
|
||||||
if rfile := c.String("requestfile"); rfile != "" {
|
if rfile := c.String("requestfile"); rfile != "" {
|
||||||
|
|
@ -151,13 +159,63 @@ func main() {
|
||||||
log.Info("HTTP endpoint opened", "url", fmt.Sprintf("http://%s", endpoint))
|
log.Info("HTTP endpoint opened", "url", fmt.Sprintf("http://%s", endpoint))
|
||||||
cors := []string{"*"}
|
cors := []string{"*"}
|
||||||
|
|
||||||
|
if c.Bool("stdio-ui-test") {
|
||||||
|
log.Info("Performing UI test")
|
||||||
|
go testExternalUI(api)
|
||||||
|
}
|
||||||
|
|
||||||
rpc.NewHTTPServer(cors, server).Serve(listener)
|
rpc.NewHTTPServer(cors, server).Serve(listener)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
app.Run(os.Args)
|
app.Run(os.Args)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testExternalUI(api *SignerAPI) {
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), "remote", "signer binary")
|
||||||
|
ctx = context.WithValue(ctx, "scheme", "in-proc")
|
||||||
|
ctx = context.WithValue(ctx, "local", "main")
|
||||||
|
|
||||||
|
errs := make([]string, 0)
|
||||||
|
|
||||||
|
api.ui.ShowInfo("Testing 'ShowInfo'")
|
||||||
|
api.ui.ShowError("Testing 'ShowError'")
|
||||||
|
|
||||||
|
checkErr:= func(method string, err error){
|
||||||
|
if err != nil && err != ErrRequestDenied{
|
||||||
|
errs = append(errs, fmt.Sprintf("%v: %v", method, err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
|
||||||
|
_, err = api.SignTransaction(ctx, common.Address{}, TransactionArg{}, nil);
|
||||||
|
checkErr("SignTransaction", err)
|
||||||
|
_, err = api.Sign(ctx, common.Address{}, common.Hex2Bytes("01020304"))
|
||||||
|
checkErr("Sign", err)
|
||||||
|
_, err =api.List(ctx)
|
||||||
|
checkErr("List", err)
|
||||||
|
_, err = api.New(ctx)
|
||||||
|
checkErr("New", err)
|
||||||
|
_, err = api.Export(ctx, common.Address{})
|
||||||
|
checkErr("Export", err)
|
||||||
|
_, err = api.Import(ctx, json.RawMessage{})
|
||||||
|
checkErr("Import", err)
|
||||||
|
|
||||||
|
api.ui.ShowInfo("Tests completed")
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
log.Error("Got errors")
|
||||||
|
for _, e := range errs {
|
||||||
|
log.Error(e)
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
log.Info("No errors")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Create Account
|
// Create Account
|
||||||
// curl -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_new","params":["test"],"id":67}' localhost:8550
|
// curl -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_new","params":["test"],"id":67}' localhost:8550
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,60 +34,117 @@ class PipeTransport(ServerTransport):
|
||||||
|
|
||||||
def receive_message(self):
|
def receive_message(self):
|
||||||
data = self.input.readline()
|
data = self.input.readline()
|
||||||
print("IN ->\n{}".format( data))
|
#print(">> {}".format( data))
|
||||||
return None, urlparse.unquote(data)
|
return None, urlparse.unquote(data)
|
||||||
|
|
||||||
def send_reply(self, context, reply):
|
def send_reply(self, context, reply):
|
||||||
print("OUT <-\n{}".format( reply))
|
#print("<< {}".format( reply))
|
||||||
self.output.write(reply)
|
self.output.write(reply)
|
||||||
self.output.write("\n")
|
self.output.write("\n")
|
||||||
|
|
||||||
dispatcher = RPCDispatcher()
|
dispatcher = RPCDispatcher()
|
||||||
|
|
||||||
@dispatcher.public
|
@dispatcher.public
|
||||||
def ApproveTx(Transaction = None, From = None, Callinfo = None, Meta = None):
|
def ApproveTx(transaction = None, fromaccount = None, call_info = None, meta = None):
|
||||||
|
"""
|
||||||
|
Example request:
|
||||||
|
|
||||||
|
{"jsonrpc":"2.0","method":"ApproveTx","params":{"transaction":{"to":null,"gas":null,"gasPrice":null,"value":null,"data":"0x","nonce":null},"from":"0x0000000000000000000000000000000000000000","call_info":null,"meta":{"remote":"signer binary","local":"main","scheme":"in-proc"}},"id":2}
|
||||||
|
|
||||||
|
:param transaction: transaction info
|
||||||
|
:param call_info: info abou the call, e.g. if ABI info could not be
|
||||||
|
:param meta: metadata about the request, e.g. where the call comes from
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
return {
|
return {
|
||||||
"Approved" : True,
|
"approved" : False,
|
||||||
"Transaction" : Transaction,
|
"transaction" : None,
|
||||||
"From" : From,
|
"fromaccount" : fromaccount,
|
||||||
"Password" : None,
|
"password" : None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@dispatcher.public
|
@dispatcher.public
|
||||||
def ApproveSignData():
|
def ApproveSignData(address=None, raw_data = None, message = None, hash = None, meta = None):
|
||||||
return {"Approved": False,
|
""" Example request
|
||||||
"Password" : None}
|
|
||||||
|
{"jsonrpc":"2.0","method":"ApproveSignData","params":{"address":"0x0000000000000000000000000000000000000000","raw_data":"0x01020304","message":"\u0019Ethereum Signed Message:\n4\u0001\u0002\u0003\u0004","hash":"0x7e3a4e7a9d1744bc5c675c25e1234ca8ed9162bd17f78b9085e48047c15ac310","meta":{"remote":"signer binary","local":"main","scheme":"in-proc"}},"id":3}
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
return {"approved": False,
|
||||||
|
"password" : None}
|
||||||
|
|
||||||
@dispatcher.public
|
@dispatcher.public
|
||||||
def ApproveExport():
|
def ApproveExport(address = None, meta = None):
|
||||||
return {"Approved" : False}
|
""" Example request
|
||||||
|
|
||||||
|
{"jsonrpc":"2.0","method":"ApproveExport","params":{"address":"0x0000000000000000000000000000000000000000","meta":{"remote":"signer binary","local":"main","scheme":"in-proc"}},"id":5}
|
||||||
|
|
||||||
|
"""
|
||||||
|
return {"approved" : False}
|
||||||
|
|
||||||
@dispatcher.public
|
@dispatcher.public
|
||||||
def ApproveImport():
|
def ApproveImport(meta = None):
|
||||||
return {"Approved" : False, "OldPassword": "", "NewPassword": ""}
|
""" Example request
|
||||||
|
|
||||||
|
{"jsonrpc":"2.0","method":"ApproveImport","params":{"Meta":{}},"id":4}
|
||||||
|
|
||||||
|
"""
|
||||||
|
return {"approved" : False, "old_password": "", "new_password": ""}
|
||||||
|
|
||||||
@dispatcher.public
|
@dispatcher.public
|
||||||
def ApproveListing():
|
def ApproveListing(accounts=None, meta = None):
|
||||||
return []
|
""" Example request
|
||||||
|
|
||||||
|
{"jsonrpc":"2.0","method":"ApproveListing","params":{"accounts":[{"type":"Account","url":"keystore:///home/user/ethereum/keystore/file","address":"0x010101010101010010101010101abcdef0001337"}],"Meta":{}},"id":2}
|
||||||
|
"""
|
||||||
|
return {'accounts': []}
|
||||||
|
|
||||||
@dispatcher.public
|
@dispatcher.public
|
||||||
def ApproveNewAccount():
|
def ApproveNewAccount(meta = None):
|
||||||
return {"Approved": False, "Password": ""}
|
"""
|
||||||
|
Example request
|
||||||
|
|
||||||
|
{"jsonrpc":"2.0","method":"ApproveNewAccount","params":{"meta":{"remote":"signer binary","local":"main","scheme":"in-proc"}},"id":5}
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return {"approved": False, "password": ""}
|
||||||
|
|
||||||
@dispatcher.public
|
@dispatcher.public
|
||||||
def ShowError(text = ""):
|
def ShowError(message = ""):
|
||||||
sys.err.println("Error: %s", text)
|
"""
|
||||||
|
Example request:
|
||||||
|
|
||||||
|
{"jsonrpc":"2.0","method":"ShowInfo","params":{"message":"Testing 'ShowError'"},"id":1}
|
||||||
|
|
||||||
|
:param text: to show
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
sys.stderr.write("Error: {}\n".format( message))
|
||||||
return
|
return
|
||||||
|
|
||||||
@dispatcher.public
|
@dispatcher.public
|
||||||
def ShowInfo(text = ""):
|
def ShowInfo(message = ""):
|
||||||
sys.err.println("Info: %s", text)
|
"""
|
||||||
|
Example request
|
||||||
|
{"jsonrpc":"2.0","method":"ShowInfo","params":{"message":"Testing 'ShowInfo'"},"id":0}
|
||||||
|
|
||||||
|
:param text: to display
|
||||||
|
:return:nothing
|
||||||
|
"""
|
||||||
|
sys.stdout.write("Info: {}\n".format( message))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main(args):
|
||||||
|
|
||||||
|
cmd = ["./signer", "--stdio-ui"]
|
||||||
|
if len(args) > 0 and args[0] == "test":
|
||||||
|
cmd.extend(["--stdio-ui-test"])
|
||||||
|
print("cmd: {}".format(" ".join(cmd)))
|
||||||
# line buffered
|
# line buffered
|
||||||
p = subprocess.Popen(["./signer", "--stdio-ui"], bufsize=1, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
p = subprocess.Popen(cmd, bufsize=1, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||||
transport = PipeTransport(p.stdout, p.stdin)
|
transport = PipeTransport(p.stdout, p.stdin)
|
||||||
rpc_server = RPCServer(
|
rpc_server = RPCServer(
|
||||||
transport,
|
transport,
|
||||||
|
|
@ -97,4 +154,4 @@ def main():
|
||||||
rpc_server.serve_forever()
|
rpc_server.serve_forever()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main(sys.argv[1:])
|
||||||
|
|
@ -109,9 +109,17 @@ func (ui StdIOUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountRespo
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui StdIOUI) ShowError(message string) {
|
func (ui StdIOUI) ShowError(message string) {
|
||||||
|
err := ui.dispatch("ShowError", &Message{message}, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Info("Error calling 'ShowError'", "exc", err.Error(),"msg", message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui StdIOUI) ShowInfo(message string) {
|
func (ui StdIOUI) ShowInfo(message string) {
|
||||||
|
err := ui.dispatch("ShowInfo", Message{message}, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Info("Error calling 'ShowInfo'", "exc", err.Error(), "msg", message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type rwc struct {
|
type rwc struct {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue