diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..76b6e545 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/ffsigner/main.go", + "args": [ + "send-transaction", + "-f", "${workspaceFolder}/test/firefly.ffsigner.yaml", + "-i", "${workspaceFolder}/test/offline-tx.json" + ] + } + ] +} \ No newline at end of file diff --git a/cmd/ffsigner.go b/cmd/ffsigner.go index 09716179..4c268b4b 100644 --- a/cmd/ffsigner.go +++ b/cmd/ffsigner.go @@ -51,6 +51,7 @@ func init() { rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "f", "", "config file") rootCmd.AddCommand(versionCommand()) rootCmd.AddCommand(configCommand()) + rootCmd.AddCommand(sendTransactionCommand()) } func Execute() error { @@ -62,7 +63,7 @@ func initConfig() { signerconfig.Reset() } -func run() error { +func initServer() (rpcserver.Server, error) { initConfig() err := config.ReadConfig("ffsigner", cfgFile) @@ -78,7 +79,7 @@ func run() error { // Deferred error return from reading config if err != nil { cancelCtx() - return i18n.WrapError(ctx, err, i18n.MsgConfigFailed) + return nil, i18n.WrapError(ctx, err, i18n.MsgConfigFailed) } // Setup signal handling to cancel the context, which shuts down the API Server @@ -90,14 +91,22 @@ func run() error { }() if !config.GetBool(signerconfig.FileWalletEnabled) { - return i18n.NewError(ctx, signermsgs.MsgNoWalletEnabled) + return nil, i18n.NewError(ctx, signermsgs.MsgNoWalletEnabled) } fileWallet, err := fswallet.NewFilesystemWallet(ctx, fswallet.ReadConfig(signerconfig.FileWalletConfig)) if err != nil { - return err + return nil, err } server, err := rpcserver.NewServer(ctx, fileWallet) + if err == nil { + err = server.Init() + } + return server, err +} + +func run() error { + server, err := initServer() if err != nil { return err } diff --git a/cmd/send_transaction.go b/cmd/send_transaction.go new file mode 100644 index 00000000..18ae4ec3 --- /dev/null +++ b/cmd/send_transaction.go @@ -0,0 +1,54 @@ +// Copyright © 2023 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/spf13/cobra" +) + +var transactionFile string + +func sendTransactionCommand() *cobra.Command { + sendTransactionCmd := &cobra.Command{ + Use: "send-transaction input-file.json", + Short: "Submits a transaction from a JSON file", + Long: `The JSON input includes the following parameters: +- abi: One or more function definitions +- method: The name of the function to invoke - required if the ABI contains more than one function +- params: The input parameters, as an object, or array +As well as ethereum signing parameters +- from: The ethereum address to use to sign - must already configured in the wallet +- to: The contract address to invoke (required - as this cannot be used for contract deploy) +- nonce: The nonce for the transaction (required) +- gas: The maximum gas limit for execution of the transaction (required) +- gasPrice +- maxPriorityFeePerGas +- maxFeePerGas +- value +`, + RunE: func(cmd *cobra.Command, args []string) error { + server, err := initServer() + if err != nil { + return err + } + return server.SignTransactionFromFile(cmd.Context(), transactionFile) + }, + } + sendTransactionCmd.Flags().StringVarP(&transactionFile, "input", "i", "", "input file") + _ = sendTransactionCmd.MarkFlagRequired("input") + return sendTransactionCmd +} diff --git a/internal/rpcserver/offline_signer.go b/internal/rpcserver/offline_signer.go new file mode 100644 index 00000000..623d59b5 --- /dev/null +++ b/internal/rpcserver/offline_signer.go @@ -0,0 +1,93 @@ +// Copyright © 2023 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rpcserver + +import ( + "context" + "encoding/json" + "os" + + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-signer/internal/signermsgs" + "github.com/hyperledger/firefly-signer/pkg/abi" + "github.com/hyperledger/firefly-signer/pkg/ethsigner" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" + "github.com/hyperledger/firefly-signer/pkg/rpcbackend" +) + +type SignTransactionInput struct { + ethsigner.Transaction + Address *ethtypes.Address0xHex `json:"address"` + ABI *abi.ABI `json:"abi"` + Method string `json:"method,omitempty"` + Params interface{} `json:"params"` +} + +func (s *rpcServer) SignTransactionFromFile(ctx context.Context, filename string) error { + + // Parse the input + var input SignTransactionInput + inputData, err := os.ReadFile(filename) + if err != nil { + return err + } + if err = json.Unmarshal(inputData, &input); err != nil { + return err + } + + // Find the function to invoke + functions := input.ABI.Functions() + var method *abi.Entry + if input.Method == "" { + if len(functions) != 1 { + return i18n.NewError(ctx, signermsgs.MsgOfflineSignMethodCount, len(functions)) + } + } else { + for _, f := range functions { + if f.String() == input.Method { + // Full signature match wins + method = f + } else if method == nil && f.Name == input.Method { + // But name is good enough (could be multiple overrides, so we don't break) + method = f + } + } + if method == nil { + return i18n.NewError(ctx, signermsgs.MsgOfflineSignMethodNotFound, input.Method) + } + } + + // Generate the transaction data + paramValues, err := method.Inputs.ParseExternalDataCtx(ctx, input.Params) + if err == nil { + input.Data, err = method.EncodeCallDataCtx(ctx, paramValues) + } + if err != nil { + return err + } + + // Sign the transaction + rpcRes, err := s.processEthTransaction(ctx, &rpcbackend.RPCRequest{}, &input.Transaction) + if err != nil { + return err + } + resBytes, _ := json.Marshal(rpcRes) + log.L(ctx).Infof("Submitted:\n%s", resBytes) + return nil + +} diff --git a/internal/rpcserver/rpchandler_test.go b/internal/rpcserver/rpchandler_test.go index facdcc46..9c070d75 100644 --- a/internal/rpcserver/rpchandler_test.go +++ b/internal/rpcserver/rpchandler_test.go @@ -59,7 +59,9 @@ func TestSignAndSendTransactionWithNonce(t *testing.T) { Result: fftypes.JSONAnyPtr(`"0x61ca9c99c1d752fb3bda568b8566edf33ba93585c64a970566e6dfb540a5cbc1"`), }, nil) - err := s.Start() + err := s.Init() + assert.NoError(t, err) + err = s.Start() assert.NoError(t, err) res, err := http.Post(url, "application/json", bytes.NewReader([]byte(`{ @@ -124,7 +126,9 @@ func TestSignAndSendTransactionWithoutNonce(t *testing.T) { Result: fftypes.JSONAnyPtr(`"0x61ca9c99c1d752fb3bda568b8566edf33ba93585c64a970566e6dfb540a5cbc1"`), }, nil) - err := s.Start() + err := s.Init() + assert.NoError(t, err) + err = s.Start() assert.NoError(t, err) res, err := http.Post(url, "application/json", bytes.NewReader([]byte(`{ @@ -175,7 +179,9 @@ func TestServeJSONRPCFail(t *testing.T) { }, }, fmt.Errorf("pop")) - err := s.Start() + err := s.Init() + assert.NoError(t, err) + err = s.Start() assert.NoError(t, err) res, err := http.Post(url, "application/json", bytes.NewReader([]byte(` @@ -237,7 +243,9 @@ func TestServeJSONRPCBatchOK(t *testing.T) { Result: fftypes.JSONAnyPtr(`"result 3"`), }, nil) - err := s.Start() + err := s.Init() + assert.NoError(t, err) + err = s.Start() assert.NoError(t, err) res, err := http.Post(url, "application/json", bytes.NewReader([]byte(`[ @@ -312,7 +320,9 @@ func TestServeJSONRPCBatchOneFailed(t *testing.T) { }, }, fmt.Errorf("pop")) - err := s.Start() + err := s.Init() + assert.NoError(t, err) + err = s.Start() assert.NoError(t, err) res, err := http.Post(url, "application/json", bytes.NewReader([]byte(`[ @@ -361,7 +371,9 @@ func TestServeJSONRPCBatchBadArray(t *testing.T) { w := s.wallet.(*ethsignermocks.Wallet) w.On("Initialize", mock.Anything).Return(nil) - err := s.Start() + err := s.Init() + assert.NoError(t, err) + err = s.Start() assert.NoError(t, err) res, err := http.Post(url, "application/json", bytes.NewReader([]byte(`[`))) @@ -392,7 +404,9 @@ func TestServeJSONRPCBatchEmptyData(t *testing.T) { w := s.wallet.(*ethsignermocks.Wallet) w.On("Initialize", mock.Anything).Return(nil) - err := s.Start() + err := s.Init() + assert.NoError(t, err) + err = s.Start() assert.NoError(t, err) res, err := http.Post(url, "application/json", bytes.NewReader([]byte(``))) @@ -423,7 +437,9 @@ func TestServeJSONRPCBatchBadJSON(t *testing.T) { w := s.wallet.(*ethsignermocks.Wallet) w.On("Initialize", mock.Anything).Return(nil) - err := s.Start() + err := s.Init() + assert.NoError(t, err) + err = s.Start() assert.NoError(t, err) res, err := http.Post(url, "application/json", bytes.NewReader([]byte(``))) diff --git a/internal/rpcserver/rpcprocessor.go b/internal/rpcserver/rpcprocessor.go index f0ea5bad..d5c20288 100644 --- a/internal/rpcserver/rpcprocessor.go +++ b/internal/rpcserver/rpcprocessor.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Kaleido, Inc. +// Copyright © 2023 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -72,6 +72,11 @@ func (s *rpcServer) processEthSendTransaction(ctx context.Context, rpcReq *rpcba return rpcbackend.RPCErrorResponse(err, rpcReq.ID, rpcbackend.RPCCodeParseError), err } + return s.processEthTransaction(ctx, rpcReq, &txn) +} + +func (s *rpcServer) processEthTransaction(ctx context.Context, rpcReq *rpcbackend.RPCRequest, txn *ethsigner.Transaction) (*rpcbackend.RPCResponse, error) { + if txn.From == nil { err := i18n.NewError(ctx, signermsgs.MsgMissingFrom) return rpcbackend.RPCErrorResponse(err, rpcReq.ID, rpcbackend.RPCCodeInvalidRequest), err @@ -94,7 +99,7 @@ func (s *rpcServer) processEthSendTransaction(ctx context.Context, rpcReq *rpcba // Sign the transaction var hexData ethtypes.HexBytes0xPrefix - hexData, err = s.wallet.Sign(ctx, &txn, s.chainID) + hexData, err := s.wallet.Sign(ctx, txn, s.chainID) if err != nil { return rpcbackend.RPCErrorResponse(err, rpcReq.ID, rpcbackend.RPCCodeInternalError), err } diff --git a/internal/rpcserver/server.go b/internal/rpcserver/server.go index 31a6cc6f..2c60a0bd 100644 --- a/internal/rpcserver/server.go +++ b/internal/rpcserver/server.go @@ -36,6 +36,8 @@ type Server interface { Start() error Stop() WaitStop() error + Init() error + SignTransactionFromFile(ctx context.Context, filename string) error } func NewServer(ctx context.Context, wallet ethsigner.Wallet) (ss Server, err error) { @@ -83,7 +85,7 @@ func (s *rpcServer) runAPIServer() { s.apiServer.ServeHTTP(s.ctx) } -func (s *rpcServer) Start() error { +func (s *rpcServer) Init() error { if s.chainID < 0 { var chainID ethtypes.HexInteger rpcErr := s.backend.CallRPC(s.ctx, &chainID, "net_version") @@ -93,10 +95,10 @@ func (s *rpcServer) Start() error { s.chainID = chainID.BigInt().Int64() } - err := s.wallet.Initialize(s.ctx) - if err != nil { - return err - } + return s.wallet.Initialize(s.ctx) +} + +func (s *rpcServer) Start() error { go s.runAPIServer() s.started = true return nil diff --git a/internal/rpcserver/server_test.go b/internal/rpcserver/server_test.go index f7782a9a..11c13b13 100644 --- a/internal/rpcserver/server_test.go +++ b/internal/rpcserver/server_test.go @@ -87,7 +87,9 @@ func TestStartStop(t *testing.T) { w := s.wallet.(*ethsignermocks.Wallet) w.On("Initialize", mock.Anything).Return(nil) - err := s.Start() + err := s.Init() + assert.NoError(t, err) + err = s.Start() assert.NoError(t, err) assert.Equal(t, int64(12345), s.chainID) @@ -105,7 +107,7 @@ func TestStartFailChainID(t *testing.T) { hi.BigInt().SetInt64(12345) }).Return(&rpcbackend.RPCError{Message: "pop"}) - err := s.Start() + err := s.Init() assert.Regexp(t, "pop", err) } @@ -123,7 +125,7 @@ func TestStartFailInitialize(t *testing.T) { w := s.wallet.(*ethsignermocks.Wallet) w.On("Initialize", mock.Anything).Return(fmt.Errorf("pop")) - err := s.Start() + err := s.Init() assert.Regexp(t, "pop", err) } diff --git a/internal/signermsgs/en_error_messges.go b/internal/signermsgs/en_error_messges.go index 5ec6fdcf..67c7b2cb 100644 --- a/internal/signermsgs/en_error_messges.go +++ b/internal/signermsgs/en_error_messges.go @@ -97,4 +97,6 @@ var ( MsgEIP712ValueNotArray = ffe("FF22078", "Value for '%s' not an array (%T)") MsgEIP712InvalidArrayLen = ffe("FF22079", "Value for '%s' must have %d entries (found %d)") MsgEIP712PrimaryTypeRequired = ffe("FF22080", "Primary type must be specified") + MsgOfflineSignMethodCount = ffe("FF22081", "Must be exactly one function when method not supplied (count=%d)") + MsgOfflineSignMethodNotFound = ffe("FF22082", "Function with signature or name '%s' not found in ABI") ) diff --git a/mocks/ethsignermocks/wallet.go b/mocks/ethsignermocks/wallet.go index e879a103..5bdd4195 100644 --- a/mocks/ethsignermocks/wallet.go +++ b/mocks/ethsignermocks/wallet.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.38.0. DO NOT EDIT. package ethsignermocks @@ -20,6 +20,10 @@ type Wallet struct { func (_m *Wallet) Close() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Close") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() @@ -34,6 +38,10 @@ func (_m *Wallet) Close() error { func (_m *Wallet) GetAccounts(ctx context.Context) ([]*ethtypes.Address0xHex, error) { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for GetAccounts") + } + var r0 []*ethtypes.Address0xHex var r1 error if rf, ok := ret.Get(0).(func(context.Context) ([]*ethtypes.Address0xHex, error)); ok { @@ -60,6 +68,10 @@ func (_m *Wallet) GetAccounts(ctx context.Context) ([]*ethtypes.Address0xHex, er func (_m *Wallet) Initialize(ctx context.Context) error { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for Initialize") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context) error); ok { r0 = rf(ctx) @@ -74,6 +86,10 @@ func (_m *Wallet) Initialize(ctx context.Context) error { func (_m *Wallet) Refresh(ctx context.Context) error { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for Refresh") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context) error); ok { r0 = rf(ctx) @@ -88,6 +104,10 @@ func (_m *Wallet) Refresh(ctx context.Context) error { func (_m *Wallet) Sign(ctx context.Context, txn *ethsigner.Transaction, chainID int64) ([]byte, error) { ret := _m.Called(ctx, txn, chainID) + if len(ret) == 0 { + panic("no return value specified for Sign") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, *ethsigner.Transaction, int64) ([]byte, error)); ok { diff --git a/mocks/rpcbackendmocks/backend.go b/mocks/rpcbackendmocks/backend.go index c98bd97d..e9d9f2b0 100644 --- a/mocks/rpcbackendmocks/backend.go +++ b/mocks/rpcbackendmocks/backend.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.38.0. DO NOT EDIT. package rpcbackendmocks @@ -21,6 +21,10 @@ func (_m *Backend) CallRPC(ctx context.Context, result interface{}, method strin _ca = append(_ca, params...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for CallRPC") + } + var r0 *rpcbackend.RPCError if rf, ok := ret.Get(0).(func(context.Context, interface{}, string, ...interface{}) *rpcbackend.RPCError); ok { r0 = rf(ctx, result, method, params...) @@ -37,6 +41,10 @@ func (_m *Backend) CallRPC(ctx context.Context, result interface{}, method strin func (_m *Backend) SyncRequest(ctx context.Context, rpcReq *rpcbackend.RPCRequest) (*rpcbackend.RPCResponse, error) { ret := _m.Called(ctx, rpcReq) + if len(ret) == 0 { + panic("no return value specified for SyncRequest") + } + var r0 *rpcbackend.RPCResponse var r1 error if rf, ok := ret.Get(0).(func(context.Context, *rpcbackend.RPCRequest) (*rpcbackend.RPCResponse, error)); ok { diff --git a/mocks/rpcservermocks/server.go b/mocks/rpcservermocks/server.go index b28604ff..6c04a289 100644 --- a/mocks/rpcservermocks/server.go +++ b/mocks/rpcservermocks/server.go @@ -1,18 +1,62 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.38.0. DO NOT EDIT. package rpcservermocks -import mock "github.com/stretchr/testify/mock" +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) // Server is an autogenerated mock type for the Server type type Server struct { mock.Mock } +// Init provides a mock function with given fields: +func (_m *Server) Init() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Init") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SignTransactionFromFile provides a mock function with given fields: ctx, filename +func (_m *Server) SignTransactionFromFile(ctx context.Context, filename string) error { + ret := _m.Called(ctx, filename) + + if len(ret) == 0 { + panic("no return value specified for SignTransactionFromFile") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, filename) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Start provides a mock function with given fields: func (_m *Server) Start() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Start") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() @@ -32,6 +76,10 @@ func (_m *Server) Stop() { func (_m *Server) WaitStop() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for WaitStop") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() diff --git a/mocks/secp256k1mocks/signer.go b/mocks/secp256k1mocks/signer.go index 435ad220..838af600 100644 --- a/mocks/secp256k1mocks/signer.go +++ b/mocks/secp256k1mocks/signer.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.38.0. DO NOT EDIT. package secp256k1mocks @@ -16,6 +16,10 @@ type Signer struct { func (_m *Signer) Sign(msgToHashAndSign []byte) (*secp256k1.SignatureData, error) { ret := _m.Called(msgToHashAndSign) + if len(ret) == 0 { + panic("no return value specified for Sign") + } + var r0 *secp256k1.SignatureData var r1 error if rf, ok := ret.Get(0).(func([]byte) (*secp256k1.SignatureData, error)); ok { diff --git a/mocks/secp256k1mocks/signer_direct.go b/mocks/secp256k1mocks/signer_direct.go index 6bf5f346..4ab31bcc 100644 --- a/mocks/secp256k1mocks/signer_direct.go +++ b/mocks/secp256k1mocks/signer_direct.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.38.0. DO NOT EDIT. package secp256k1mocks @@ -16,6 +16,10 @@ type SignerDirect struct { func (_m *SignerDirect) Sign(msgToHashAndSign []byte) (*secp256k1.SignatureData, error) { ret := _m.Called(msgToHashAndSign) + if len(ret) == 0 { + panic("no return value specified for Sign") + } + var r0 *secp256k1.SignatureData var r1 error if rf, ok := ret.Get(0).(func([]byte) (*secp256k1.SignatureData, error)); ok { @@ -42,6 +46,10 @@ func (_m *SignerDirect) Sign(msgToHashAndSign []byte) (*secp256k1.SignatureData, func (_m *SignerDirect) SignDirect(message []byte) (*secp256k1.SignatureData, error) { ret := _m.Called(message) + if len(ret) == 0 { + panic("no return value specified for SignDirect") + } + var r0 *secp256k1.SignatureData var r1 error if rf, ok := ret.Get(0).(func([]byte) (*secp256k1.SignatureData, error)); ok { diff --git a/test/offline-signer.conf.yaml b/test/offline-signer.conf.yaml new file mode 100644 index 00000000..9ce08ce5 --- /dev/null +++ b/test/offline-signer.conf.yaml @@ -0,0 +1,11 @@ +fileWallet: + path: "./test/keystore_toml" + disableListener: true + filenames: + with0xPrefix: false + primaryExt: '.key.json' + passwordExt: '.pwd' +backend: + url: http://localhost:5100 +log: + level: debug \ No newline at end of file diff --git a/test/offline-tx.json b/test/offline-tx.json new file mode 100644 index 00000000..207b0c84 --- /dev/null +++ b/test/offline-tx.json @@ -0,0 +1,35 @@ +{ + "from": "0x1f185718734552d08278aa70f804580bab5fd2b4", + "to": "0xd7761f80fD6C425eE275103c5b69076Ae23d6f1c", + "abi": [ + { + "constant": false, + "inputs": [ + { + "name": "to", + "type": "address" + }, + { + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } + ], + "method": "transfer", + "params": { + "to": "0x1f185718734552d08278aa70f804580bab5fd2b4", + "value": "100000000000000000" + }, + "gas": "100000" +} \ No newline at end of file