diff --git a/cli/app.go b/cli/app.go index ebb68a47..8d0a3d3a 100644 --- a/cli/app.go +++ b/cli/app.go @@ -4,7 +4,9 @@ import ( "fmt" "os" + "github.com/dcb9/janus/pkg/qtum" "github.com/dcb9/janus/pkg/server" + "github.com/dcb9/janus/pkg/transformer" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/pkg/errors" @@ -28,15 +30,20 @@ func action(pc *kingpin.ParseContext) error { logger = level.NewFilter(logger, level.AllowWarn()) } - s, err := server.New( - *qtumRPC, - addr, - server.SetLogger(logger), - server.SetDebug(*devMode), - ) + qtumJSONRPC, err := qtum.NewClient(*qtumRPC, qtum.SetDebug(*devMode), qtum.SetLogger(logger)) + if err != nil { + return errors.Wrap(err, "jsonrpc#New") + } + qtumClient := qtum.New(qtumJSONRPC) + + t, err := transformer.New(qtumClient, transformer.DefaultProxies(qtumClient), transformer.SetDebug(*devMode), transformer.SetLogger(logger)) + if err != nil { + return errors.Wrap(err, "transformer#New") + } + s, err := server.New(qtumClient, t, addr, server.SetLogger(logger), server.SetDebug(*devMode)) if err != nil { - return errors.Wrap(err, "new proxy") + return errors.Wrap(err, "server#New") } return s.Start() diff --git a/pkg/eth/eth.go b/pkg/eth/eth.go index 8768ac9a..ba834bc0 100644 --- a/pkg/eth/eth.go +++ b/pkg/eth/eth.go @@ -2,180 +2,45 @@ package eth import ( "encoding/json" - "errors" - "math/big" - "strings" - - "github.com/dcb9/janus/pkg/rpc" - "github.com/ethereum/go-ethereum/common/hexutil" + "fmt" ) -func NewJSONRPCResult(id, rawResult json.RawMessage, err *rpc.JSONRPCError) *rpc.JSONRPCResult { - return &rpc.JSONRPCResult{ - JSONRPC: "2.0", - ID: id, - RawResult: rawResult, - Error: err, - } -} - -// eth_sendTransaction -type TransactionReq struct { - From string `json:"from"` - To string `json:"to"` - Gas *EthInt `json:"gas"` // optional - GasPrice *EthInt `json:"gasPrice"` // optional - Value string `json:"value"` // optional - Data string `json:"data"` // optional - Nonce string `json:"nonce"` // optional -} - -// see: https://ethereum.stackexchange.com/questions/8384/transfer-an-amount-between-two-ethereum-accounts-using-json-rpc -func (t *TransactionReq) IsSendEther() bool { - // data must be empty - return t.Value != "" && t.To != "" && t.From != "" && t.Data == "" -} - -func (t *TransactionReq) IsCreateContract() bool { - return t.To == "" && t.Data != "" -} - -func (t *TransactionReq) IsCallContract() bool { - return t.To != "" && t.Data != "" -} - -// FIXME: GetGas -> GasHex -func (t *TransactionReq) GetGas() string { - return t.Gas.Hex() -} - -// FIXME: GetGasPrice -> GasPriceHex -func (t *TransactionReq) GetGasPrice() string { - return t.GasPrice.Hex() -} - -// eth_call -type TransactionCallReq struct { - From string `json:"from"` - To string `json:"to"` - Gas *EthInt `json:"gas"` // optional - GasPrice *EthInt `json:"gasPrice"` // optional - Value string `json:"value"` // optional - Data string `json:"data"` // optional -} - -func (t *TransactionCallReq) GetGas() string { - return t.Gas.Hex() -} - -func (t *TransactionCallReq) GetGasPrice() string { - return t.GasPrice.Hex() -} - -type ( - Log struct { - Removed string `json:"removed,omitempty"` // TAG - true when the log was removed, due to a chain reorganization. false if its a valid log. - LogIndex string `json:"logIndex"` // QUANTITY - integer of the log index position in the block. null when its pending log. - TransactionIndex string `json:"transactionIndex"` // QUANTITY - integer of the transactions index position log was created from. null when its pending log. - TransactionHash string `json:"transactionHash"` // DATA, 32 Bytes - hash of the transactions this log was created from. null when its pending log. - BlockHash string `json:"blockHash"` // DATA, 32 Bytes - hash of the block where this log was in. null when its pending. null when its pending log. - BlockNumber string `json:"blockNumber"` // QUANTITY - the block number where this log was in. null when its pending. null when its pending log. - Address string `json:"address"` // DATA, 20 Bytes - address from which this log originated. - Data string `json:"data"` // DATA - contains one or more 32 Bytes non-indexed arguments of the log. - Topics []string `json:"topics"` // Array of DATA - Array of 0 to 4 32 Bytes DATA of indexed log arguments. - Type string `json:"type,omitempty"` - } - - TransactionReceipt struct { - TransactionHash string `json:"transactionHash"` // DATA, 32 Bytes - hash of the transaction. - TransactionIndex string `json:"transactionIndex"` // QUANTITY - integer of the transactions index position in the block. - BlockHash string `json:"blockHash"` // DATA, 32 Bytes - hash of the block where this transaction was in. - BlockNumber string `json:"blockNumber"` // QUANTITY - block number where this transaction was in. - From string `json:"from,omitempty"` // DATA, 20 Bytes - address of the sender. - To string `json:"to,omitempty"` // DATA, 20 Bytes - address of the receiver. null when its a contract creation transaction. - CumulativeGasUsed string `json:"cumulativeGasUsed"` // QUANTITY - The total amount of gas used when this transaction was executed in the block. - GasUsed string `json:"gasUsed"` // QUANTITY - The amount of gas used by this specific transaction alone. - ContractAddress string `json:"contractAddress"` // DATA, 20 Bytes - The contract address created, if the transaction was a contract creation, otherwise null. - Logs []Log `json:"logs"` // Array - Array of log objects, which this transaction generated. - LogsBloom string `json:"logsBloom"` // DATA, 256 Bytes - Bloom filter for light clients to quickly retrieve related logs. - Root string `json:"root,omitempty"` // DATA 32 bytes of post-transaction stateroot (pre Byzantium) - Status string `json:"status"` // QUANTITY either 1 (success) or 0 (failure) - } - - TransactionResponse struct { - Hash string `json:"hash"` // DATA, 32 Bytes - hash of the transaction. - Nonce string `json:"nonce"` // QUANTITY - the number of transactions made by the sender prior to this one. - BlockHash string `json:"blockHash"` // DATA, 32 Bytes - hash of the block where this transaction was in. null when its pending. - BlockNumber string `json:"blockNumber"` // QUANTITY - block number where this transaction was in. null when its pending. - TransactionIndex string `json:"transactionIndex"` // QUANTITY - integer of the transactions index position in the block. null when its pending. - From string `json:"from"` // DATA, 20 Bytes - address of the sender. - To string `json:"to"` // DATA, 20 Bytes - address of the receiver. null when its a contract creation transaction. - Value string `json:"value"` // QUANTITY - value transferred in Wei. - GasPrice string `json:"gasPrice"` // QUANTITY - gas price provided by the sender in Wei. - Gas string `json:"gas"` // QUANTITY - gas provided by the sender. - Input string `json:"input"` // DATA - the data send along with the transaction. - } - - GetLogsFilter struct { - FromBlock json.RawMessage `json:"fromBlock"` - ToBlock json.RawMessage `json:"toBlock"` - Address json.RawMessage `json:"address"` // string or []string - Topics []string `json:"topics"` - Blockhash string `json:"blockhash"` - } +const ( + RPCVersion = "2.0" ) -// FIXME: ETHInt -type EthInt big.Int - -func (i *EthInt) Hex() string { - return hexutil.EncodeBig(i.ToBigInt()) +type JSONRPCRequest struct { + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + ID json.RawMessage `json:"id"` + Params json.RawMessage `json:"params"` } -func (i *EthInt) ToBigInt() *big.Int { - v := *i - vv := big.Int(v) - return &vv +type JSONRPCResult struct { + JSONRPC string `json:"jsonrpc"` + RawResult json.RawMessage `json:"result,omitempty"` + Error *JSONRPCError `json:"error,omitempty"` + ID json.RawMessage `json:"id"` } -func (i *EthInt) MarshalJSON() ([]byte, error) { - return json.Marshal(i.ToBigInt()) +type JSONRPCError struct { + Code int `json:"code"` + Message string `json:"message"` } -// FIXME: extract parsers into constructors: -// ETHIntFromNumber -// ETHIntFromIntger - -// UnmarshalJSON needs to be able to parse ETHInt from both hex string or number -func (i *EthInt) UnmarshalJSON(data []byte) (err error) { - if len(data) == 0 { - return errors.New("data must not be empty") - } - - if data[0] != '"' && data[len(data)-1] != '"' { - var v *big.Int - if err = json.Unmarshal(data, &v); err != nil { - return err - } - vv := *v - *i = EthInt(vv) - return - } - - // hex - var val string - if err = json.Unmarshal(data, &val); err != nil { - return err - } - if !strings.HasPrefix(val, "0x") { - val = "0x" + val - } +func (err *JSONRPCError) Error() string { + return fmt.Sprintf("eth [code: %d] %s", err.Code, err.Message) +} - v, err := hexutil.DecodeBig(val) +func NewJSONRPCResult(id json.RawMessage, res interface{}) (*JSONRPCResult, error) { + rawResult, err := json.Marshal(res) if err != nil { - return err + return nil, err } - vv := *v - *i = EthInt(vv) - return err + + return &JSONRPCResult{ + JSONRPC: RPCVersion, + ID: id, + RawResult: rawResult, + }, nil } diff --git a/pkg/eth/eth_int.go b/pkg/eth/eth_int.go new file mode 100644 index 00000000..4b1eb756 --- /dev/null +++ b/pkg/eth/eth_int.go @@ -0,0 +1,63 @@ +package eth + +import ( + "encoding/json" + "errors" + "math/big" + + "github.com/dcb9/janus/pkg/utils" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +type ETHInt struct { + *big.Int +} + +func (i *ETHInt) Hex() string { + return hexutil.EncodeBig(i.Int) +} + +func (i *ETHInt) MarshalJSON() ([]byte, error) { + return json.Marshal(i.Int) +} + +// UnmarshalJSON needs to be able to parse ETHInt from both hex string or number +func (i *ETHInt) UnmarshalJSON(data []byte) (err error) { + if len(data) == 0 { + return errors.New("data must not be empty") + } + + isNumber := func(data []byte) bool { + return data[0] != '"' && data[len(data)-1] != '"' + } + + if isNumber(data) { + i.Int, err = bigIntFromNumber(data) + return err + } + + i.Int, err = bigIntFromHex(data) + return err +} + +func bigIntFromNumber(data json.RawMessage) (*big.Int, error) { + var v *big.Int + if err := json.Unmarshal(data, &v); err != nil { + return nil, err + } + return v, nil +} + +func bigIntFromHex(data json.RawMessage) (*big.Int, error) { + var val string + + if err := json.Unmarshal(data, &val); err != nil { + return nil, err + } + + i, err := utils.DecodeBig(val) + if err != nil { + return nil, err + } + return i, nil +} diff --git a/pkg/eth/rpc_types.go b/pkg/eth/rpc_types.go new file mode 100644 index 00000000..22bb776d --- /dev/null +++ b/pkg/eth/rpc_types.go @@ -0,0 +1,216 @@ +package eth + +import ( + "encoding/json" + "errors" +) + +type ( + SendTransactionResponse string + + // SendTransactionRequest eth_sendTransaction + SendTransactionRequest struct { + From string `json:"from"` + To string `json:"to"` + Gas *ETHInt `json:"gas"` // optional + GasPrice *ETHInt `json:"gasPrice"` // optional + Value string `json:"value"` // optional + Data string `json:"data"` // optional + Nonce string `json:"nonce"` // optional + } +) + +func (r *SendTransactionRequest) UnmarshalJSON(data []byte) error { + type Request SendTransactionRequest + + var params []Request + if err := json.Unmarshal(data, ¶ms); err != nil { + return err + } + + *r = SendTransactionRequest(params[0]) + + return nil +} + +// see: https://ethereum.stackexchange.com/questions/8384/transfer-an-amount-between-two-ethereum-accounts-using-json-rpc +func (t *SendTransactionRequest) IsSendEther() bool { + // data must be empty + return t.Value != "" && t.To != "" && t.From != "" && t.Data == "" +} + +func (t *SendTransactionRequest) IsCreateContract() bool { + return t.To == "" && t.Data != "" +} + +func (t *SendTransactionRequest) IsCallContract() bool { + return t.To != "" && t.Data != "" +} + +func (t *SendTransactionRequest) GasHex() string { + return t.Gas.Hex() +} + +func (t *SendTransactionRequest) GasPriceHex() string { + return t.GasPrice.Hex() +} + +// CallResponse +type CallResponse string + +// CallRequest eth_call +type CallRequest struct { + From string `json:"from"` + To string `json:"to"` + Gas *ETHInt `json:"gas"` // optional + GasPrice *ETHInt `json:"gasPrice"` // optional + Value string `json:"value"` // optional + Data string `json:"data"` // optional +} + +func (t *CallRequest) GasHex() string { + return t.Gas.Hex() +} + +func (t *CallRequest) GasPriceHex() string { + return t.GasPrice.Hex() +} + +func (t *CallRequest) UnmarshalJSON(data []byte) error { + var err error + var params []json.RawMessage + if err = json.Unmarshal(data, ¶ms); err != nil { + return err + } + + if len(params) == 0 { + return errors.New("params must be set") + } + + type txCallObject CallRequest + var obj txCallObject + if err = json.Unmarshal(params[0], &obj); err != nil { + return err + } + + cr := CallRequest(obj) + *t = cr + return nil +} + +type ( + PersonalUnlockAccountResponse bool + BlockNumberResponse string + NetVersionResponse string +) + +// ========== GetLogs ============= // + +type ( + GetLogsRequest struct { + FromBlock json.RawMessage `json:"fromBlock"` + ToBlock json.RawMessage `json:"toBlock"` + Address json.RawMessage `json:"address"` // string or []string + Topics []string `json:"topics"` + Blockhash string `json:"blockhash"` + } + GetLogsResponse []Log +) + +func (r *GetLogsRequest) UnmarshalJSON(data []byte) error { + type Request GetLogsRequest + var params []Request + if err := json.Unmarshal(data, ¶ms); err != nil { + return err + } + + if len(params) == 0 { + return errors.New("params must be set") + } + + *r = GetLogsRequest(params[0]) + + return nil +} + +// ========== GetTransactionByHash ============= // +type ( + GetTransactionByHashRequest string + GetTransactionByHashResponse struct { + Hash string `json:"hash"` // DATA, 32 Bytes - hash of the transaction. + Nonce string `json:"nonce"` // QUANTITY - the number of transactions made by the sender prior to this one. + BlockHash string `json:"blockHash"` // DATA, 32 Bytes - hash of the block where this transaction was in. null when its pending. + BlockNumber string `json:"blockNumber"` // QUANTITY - block number where this transaction was in. null when its pending. + TransactionIndex string `json:"transactionIndex"` // QUANTITY - integer of the transactions index position in the block. null when its pending. + From string `json:"from"` // DATA, 20 Bytes - address of the sender. + To string `json:"to"` // DATA, 20 Bytes - address of the receiver. null when its a contract creation transaction. + Value string `json:"value"` // QUANTITY - value transferred in Wei. + GasPrice string `json:"gasPrice"` // QUANTITY - gas price provided by the sender in Wei. + Gas string `json:"gas"` // QUANTITY - gas provided by the sender. + Input string `json:"input"` // DATA - the data send along with the transaction. + } +) + +func (r *GetTransactionByHashRequest) UnmarshalJSON(data []byte) error { + var params []string + err := json.Unmarshal(data, ¶ms) + if err != nil { + return err + } + + if len(params) == 0 { + return errors.New("params must be set") + } + + *r = GetTransactionByHashRequest(params[0]) + return nil +} + +// ========== GetTransactionReceipt ============= // + +type ( + GetTransactionReceiptRequest string + GetTransactionReceiptResponse struct { + TransactionHash string `json:"transactionHash"` // DATA, 32 Bytes - hash of the transaction. + TransactionIndex string `json:"transactionIndex"` // QUANTITY - integer of the transactions index position in the block. + BlockHash string `json:"blockHash"` // DATA, 32 Bytes - hash of the block where this transaction was in. + BlockNumber string `json:"blockNumber"` // QUANTITY - block number where this transaction was in. + From string `json:"from,omitempty"` // DATA, 20 Bytes - address of the sender. + To string `json:"to,omitempty"` // DATA, 20 Bytes - address of the receiver. null when its a contract creation transaction. + CumulativeGasUsed string `json:"cumulativeGasUsed"` // QUANTITY - The total amount of gas used when this transaction was executed in the block. + GasUsed string `json:"gasUsed"` // QUANTITY - The amount of gas used by this specific transaction alone. + ContractAddress string `json:"contractAddress"` // DATA, 20 Bytes - The contract address created, if the transaction was a contract creation, otherwise null. + Logs []Log `json:"logs"` // Array - Array of log objects, which this transaction generated. + LogsBloom string `json:"logsBloom"` // DATA, 256 Bytes - Bloom filter for light clients to quickly retrieve related logs. + Root string `json:"root,omitempty"` // DATA 32 bytes of post-transaction stateroot (pre Byzantium) + Status string `json:"status"` // QUANTITY either 1 (success) or 0 (failure) + } + + Log struct { + Removed string `json:"removed,omitempty"` // TAG - true when the log was removed, due to a chain reorganization. false if its a valid log. + LogIndex string `json:"logIndex"` // QUANTITY - integer of the log index position in the block. null when its pending log. + TransactionIndex string `json:"transactionIndex"` // QUANTITY - integer of the transactions index position log was created from. null when its pending log. + TransactionHash string `json:"transactionHash"` // DATA, 32 Bytes - hash of the transactions this log was created from. null when its pending log. + BlockHash string `json:"blockHash"` // DATA, 32 Bytes - hash of the block where this log was in. null when its pending. null when its pending log. + BlockNumber string `json:"blockNumber"` // QUANTITY - the block number where this log was in. null when its pending. null when its pending log. + Address string `json:"address"` // DATA, 20 Bytes - address from which this log originated. + Data string `json:"data"` // DATA - contains one or more 32 Bytes non-indexed arguments of the log. + Topics []string `json:"topics"` // Array of DATA - Array of 0 to 4 32 Bytes DATA of indexed log arguments. + Type string `json:"type,omitempty"` + } +) + +func (r *GetTransactionReceiptRequest) UnmarshalJSON(data []byte) error { + var params []string + err := json.Unmarshal(data, ¶ms) + if err != nil { + return err + } + + if len(params) == 0 { + return errors.New("params must be set") + } + + *r = GetTransactionReceiptRequest(params[0]) + return nil +} diff --git a/pkg/qtum/btcasm.go b/pkg/qtum/btcasm.go new file mode 100644 index 00000000..82eef1a5 --- /dev/null +++ b/pkg/qtum/btcasm.go @@ -0,0 +1,87 @@ +package qtum + +import ( + "math/big" + "strings" + + "github.com/pkg/errors" +) + +type ( + // ASM is Bitcoin Script extended by Qtum to support smart contracts + ASM struct { + VMVersion string + GasLimitStr string + GasPriceStr string + Instructor string + } + CallASM struct { + ASM + callData string + ContractAddress string + } + CreateASM struct { + ASM + callData string + } +) + +func (asm *ASM) GasPrice() (*big.Int, error) { + return stringNumberToBigInt(asm.GasPriceStr) +} + +func (asm *ASM) GasLimit() (*big.Int, error) { + return stringNumberToBigInt(asm.GasLimitStr) +} + +func (asm *CreateASM) CallData() string { + return asm.callData +} + +func (asm *CallASM) CallData() string { + return asm.callData +} + +func ParseCreateASM(asm string) (*CreateASM, error) { + parts := strings.Split(asm, " ") + if len(parts) < 5 { + return nil, errors.New("invalid create ASM") + } + + return &CreateASM{ + ASM: ASM{ + VMVersion: parts[0], + GasLimitStr: parts[1], + GasPriceStr: parts[2], + Instructor: parts[4], + }, + callData: parts[3], + }, nil +} + +func ParseCallASM(asm string) (*CallASM, error) { + parts := strings.Split(asm, " ") + if len(parts) < 6 { + return nil, errors.New("invalid call ASM") + } + + return &CallASM{ + ASM: ASM{ + VMVersion: parts[0], + GasLimitStr: parts[1], + GasPriceStr: parts[2], + Instructor: parts[5], + }, + callData: parts[3], + ContractAddress: parts[4], + }, nil +} + +func stringNumberToBigInt(str string) (*big.Int, error) { + var success bool + v := new(big.Int) + if v, success = v.SetString(str, 10); !success { + return nil, errors.Errorf("Failed to parse big.Int: %s", str) + } + return v, nil +} diff --git a/pkg/qtum/qtum_test.go b/pkg/qtum/btcasm_test.go similarity index 68% rename from pkg/qtum/qtum_test.go rename to pkg/qtum/btcasm_test.go index d668918e..e647cba5 100644 --- a/pkg/qtum/qtum_test.go +++ b/pkg/qtum/btcasm_test.go @@ -14,12 +14,12 @@ func TestParseCallASM(t *testing.T) { } want := &CallASM{ ASM: ASM{ - VMVersion: "4", - GasLimit: "250000", - GasPrice: "40", - Instructor: "OP_CALL", + VMVersion: "4", + GasLimitStr: "250000", + GasPriceStr: "40", + Instructor: "OP_CALL", }, - EncodedABI: "60fe47b10000000000000000000000000000000000000000000000000000000000000002", + callData: "60fe47b10000000000000000000000000000000000000000000000000000000000000002", ContractAddress: "cd20af1f2d6ac4173f9464030e7cef40bf9cb7c4", } if !reflect.DeepEqual(got, want) { @@ -40,12 +40,12 @@ func TestParseCreateASM(t *testing.T) { } want := &CreateASM{ ASM: ASM{ - VMVersion: "4", - GasLimit: "6721975", - GasPrice: "40", - Instructor: "OP_CREATE", + VMVersion: "4", + GasLimitStr: "6721975", + GasPriceStr: "40", + Instructor: "OP_CREATE", }, - EncodedABI: "608060405234801561001057600080fd5b506040516020806100f2833981016040525160005560bf806100336000396000f30060806040526004361060485763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166360fe47b18114604d5780636d4ce63c146064575b600080fd5b348015605857600080fd5b5060626004356088565b005b348015606f57600080fd5b506076608d565b60408051918252519081900360200190f35b600055565b600054905600a165627a7a7230582049a087087e1fc6da0b68ca259d45a2e369efcbb50e93f9b7fa3e198de6402b8100290000000000000000000000000000000000000000000000000000000000000001", + callData: "608060405234801561001057600080fd5b506040516020806100f2833981016040525160005560bf806100336000396000f30060806040526004361060485763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166360fe47b18114604d5780636d4ce63c146064575b600080fd5b348015605857600080fd5b5060626004356088565b005b348015606f57600080fd5b506076608d565b60408051918252519081900360200190f35b600055565b600054905600a165627a7a7230582049a087087e1fc6da0b68ca259d45a2e369efcbb50e93f9b7fa3e198de6402b8100290000000000000000000000000000000000000000000000000000000000000001", } if !reflect.DeepEqual(got, want) { t.Errorf( diff --git a/pkg/qtum/client.go b/pkg/qtum/client.go index 71bee2fa..d523f7bc 100644 --- a/pkg/qtum/client.go +++ b/pkg/qtum/client.go @@ -1,185 +1,122 @@ package qtum import ( + "bytes" "encoding/json" - "fmt" + "io" + "io/ioutil" "math/big" "net/http" + "net/url" "sync" - "bytes" - - "io" - "io/ioutil" - - "github.com/bitly/go-simplejson" - "github.com/dcb9/janus/pkg/rpc" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/pkg/errors" ) -// FIXME: Abstract all API calls into a RPC call helper. See: -// https://github.com/hayeah/eostools/blob/8e1d0d48c74b7ca6a74b132d619a4e7f8673d26a/eos-actions/main.go#L16 - -// FIXME: We can probably remove all methods from Client, and only provide a single `Request` method +type Client struct { + URL string + doer doer -// FIXME: Define all RPC types in rpcTypes.go + logger log.Logger + debug bool -// FIXME: rename Client -> RPC or RPCClient -type Client struct { - rpcURL string - doer doer - logger log.Logger - debug bool id *big.Int + idStep *big.Int idMutex sync.Mutex } -type doer interface { - Do(*http.Request) (*http.Response, error) -} - func NewClient(rpcURL string, opts ...func(*Client) error) (*Client, error) { + err := checkRPCURL(rpcURL) + if err != nil { + return nil, err + } + c := &Client{ doer: http.DefaultClient, - rpcURL: rpcURL, + URL: rpcURL, logger: log.NewNopLogger(), debug: false, id: big.NewInt(0), + idStep: big.NewInt(1), } + for _, opt := range opts { if err := opt(c); err != nil { return nil, err } } - return c, nil -} -func SetDoer(d doer) func(*Client) error { - return func(c *Client) error { - c.doer = d - return nil - } -} -func SetDebug(debug bool) func(*Client) error { - return func(c *Client) error { - c.debug = debug - return nil - } -} - -func SetLogger(l log.Logger) func(*Client) error { - return func(c *Client) error { - c.logger = log.WithPrefix(l, "component", "qtum.Client") - return nil - } + return c, nil } -// FIXME: GetHexAddress -> Base58AddressToHex -func (c *Client) GetHexAddress(addr string) (string, error) { - r := c.NewRPCRequest(MethodGethexaddress) - r.Params = json.RawMessage(fmt.Sprintf(`["%s"]`, addr)) - - res, err := c.Request(r) +func (c *Client) Request(method string, params interface{}, result interface{}) error { + r, err := c.NewRPCRequest(method, params) if err != nil { - return "", err - } - - var hexAddr string - if err = json.Unmarshal(res.RawResult, &hexAddr); err != nil { - return "", err + return err } - return hexAddr, nil -} - -func (c *Client) FromHexAddress(addr string) (string, error) { - level.Debug(c.logger).Log("fromHexAddress", addr) - r := c.NewRPCRequest(MethodFromhexaddress) - r.Params = json.RawMessage(fmt.Sprintf(`["%s"]`, addr)) - res, err := c.Request(r) + resp, err := c.Do(r) if err != nil { - return "", err + return err } - var qtumAddr string - if err = json.Unmarshal(res.RawResult, &qtumAddr); err != nil { - return "", err + if err = json.Unmarshal(resp.RawResult, result); err != nil { + return err } - return qtumAddr, nil + return nil } -// FIXME: Define the types for all API methods: [methodName]Request, [methodName]Response -// -// type GetTransactionReceiptResponse []struct { -// // A int `json:"a"` -// } - -func (c *Client) GetTransactionReceipt(txHash string) (*TransactionReceipt, error) { - r := c.NewRPCRequest(MethodGettransactionreceipt) - r.Params = json.RawMessage(fmt.Sprintf(`["%s"]`, txHash)) - - result, err := c.Request(r) +func (c *Client) Do(req *JSONRPCRequest) (*SuccessJSONRPCResult, error) { + reqBody, err := json.Marshal(req) if err != nil { return nil, err } - js, err := simplejson.NewJson(result.RawResult) - if err != nil { - return nil, err + l := log.With(level.Debug(c.logger), "method", req.Method) + if c.debug { + l.Log("reqBody", reqBody) } - receiptJSON, err := js.GetIndex(0).Encode() - if err != nil { - return nil, err - } - var receipt *TransactionReceipt - err = json.Unmarshal(receiptJSON, &receipt) + + respBody, err := c.do(bytes.NewReader(reqBody)) if err != nil { - return nil, err + return nil, errors.Wrap(err, "Client#do") } - return receipt, nil -} - -func (c *Client) DecodeRawTransaction(hex string) (*DecodedRawTransaction, error) { - r := c.NewRPCRequest(MethodDecoderawtransaction) - r.Params = json.RawMessage(fmt.Sprintf(`["%s"]`, hex)) - - result, err := c.Request(r) - if err != nil { - return nil, err + if c.debug { + l.Log("respBody", respBody) } - var tx *DecodedRawTransaction - if err = json.Unmarshal(result.RawResult, &tx); err != nil { - return nil, err + res, err := responseBodyToResult(respBody) + if err != nil { + return nil, errors.Wrap(err, "responseBodyToResult") } - return tx, nil + return res, nil } -func (c *Client) Request(reqBody *rpc.JSONRPCRequest) (*rpc.SuccessJSONRPCResult, error) { - body, err := json.Marshal(reqBody) +func (c *Client) NewRPCRequest(method string, params interface{}) (*JSONRPCRequest, error) { + paramsJSON, err := json.Marshal(params) if err != nil { return nil, err } - respBody, err := c.do(bytes.NewReader(body)) - if err != nil { - return nil, errors.Wrap(err, "Client#do") - } - res, err := responseBodyToResult(respBody) - if err != nil { - return nil, errors.Wrap(err, "responseBodyToResult") - } + c.idMutex.Lock() + c.id = c.id.Add(c.id, c.idStep) + c.idMutex.Unlock() - return res, nil + return &JSONRPCRequest{ + JSONRPC: RPCVersion, + ID: json.RawMessage(`"` + c.id.String() + `"`), + Method: method, + Params: paramsJSON, + }, nil } func (c *Client) do(body io.Reader) ([]byte, error) { - req, err := http.NewRequest(http.MethodPost, c.rpcURL, body) + req, err := http.NewRequest(http.MethodPost, c.URL, body) if err != nil { return nil, err } @@ -198,21 +135,33 @@ func (c *Client) do(body io.Reader) ([]byte, error) { return ioutil.ReadAll(resp.Body) } -var step = big.NewInt(1) +type doer interface { + Do(*http.Request) (*http.Response, error) +} -func (c *Client) NewRPCRequest(method string) *rpc.JSONRPCRequest { - c.idMutex.Lock() - c.id = c.id.Add(c.id, step) - c.idMutex.Unlock() - return &rpc.JSONRPCRequest{ - JSONRPC: Version, - ID: json.RawMessage(`"` + c.id.String() + `"`), - Method: method, +func SetDoer(d doer) func(*Client) error { + return func(c *Client) error { + c.doer = d + return nil + } +} + +func SetDebug(debug bool) func(*Client) error { + return func(c *Client) error { + c.debug = debug + return nil + } +} + +func SetLogger(l log.Logger) func(*Client) error { + return func(c *Client) error { + c.logger = log.WithPrefix(l, "component", "qtum.Client") + return nil } } -func responseBodyToResult(body []byte) (*rpc.SuccessJSONRPCResult, error) { - var res *rpc.JSONRPCResult +func responseBodyToResult(body []byte) (*SuccessJSONRPCResult, error) { + var res *JSONRPCResult if err := json.Unmarshal(body, &res); err != nil { return nil, err } @@ -221,9 +170,26 @@ func responseBodyToResult(body []byte) (*rpc.SuccessJSONRPCResult, error) { return nil, res.Error } - return &rpc.SuccessJSONRPCResult{ + return &SuccessJSONRPCResult{ ID: res.ID, RawResult: res.RawResult, JSONRPC: res.JSONRPC, }, nil } + +func checkRPCURL(u string) error { + if u == "" { + return errors.New("URL must be set") + } + + qtumRPC, err := url.Parse(u) + if err != nil { + return errors.Errorf("QTUM_RPC URL: %s", u) + } + + if qtumRPC.User == nil { + return errors.Errorf("QTUM_RPC URL (must specify user & password): %s", u) + } + + return nil +} diff --git a/pkg/qtum/jsonrpc.go b/pkg/qtum/jsonrpc.go new file mode 100644 index 00000000..3f30a9db --- /dev/null +++ b/pkg/qtum/jsonrpc.go @@ -0,0 +1,55 @@ +package qtum + +import ( + "encoding/json" + "fmt" +) + +const ( + RPCVersion = "1.0" +) + +const ( + MethodGetHexAddress = "gethexaddress" + MethodFromHexAddress = "fromhexaddress" + MethodSendToContract = "sendtocontract" + MethodGetTransactionReceipt = "gettransactionreceipt" + MethodGetTransaction = "gettransaction" + MethodCreateContract = "createcontract" + MethodSendToAddress = "sendtoaddress" + MethodCallContract = "callcontract" + MethodDecodeRawTransaction = "decoderawtransaction" + MethodGetBlockCount = "getblockcount" + MethodGetBlockChainInfo = "getblockchaininfo" + MethodSearchLogs = "searchlogs" + MethodWaitForLogs = "waitforlogs" +) + +type JSONRPCRequest struct { + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + ID json.RawMessage `json:"id"` + Params json.RawMessage `json:"params"` +} + +type JSONRPCResult struct { + JSONRPC string `json:"jsonrpc"` + RawResult json.RawMessage `json:"result,omitempty"` + Error *JSONRPCError `json:"error,omitempty"` + ID json.RawMessage `json:"id"` +} + +type JSONRPCError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +func (err *JSONRPCError) Error() string { + return fmt.Sprintf("qtum [code: %d] %s", err.Code, err.Message) +} + +type SuccessJSONRPCResult struct { + JSONRPC string `json:"jsonrpc"` + RawResult json.RawMessage `json:"result"` + ID json.RawMessage `json:"id"` +} diff --git a/pkg/qtum/method.go b/pkg/qtum/method.go new file mode 100644 index 00000000..c25f12bd --- /dev/null +++ b/pkg/qtum/method.go @@ -0,0 +1,49 @@ +package qtum + +import "github.com/dcb9/janus/pkg/utils" + +type Method struct { + *Client +} + +func (m *Method) Base58AddressToHex(addr string) (string, error) { + var response GetHexAddressResponse + err := m.Request(MethodGetHexAddress, GetHexAddressRequest(addr), &response) + if err != nil { + return "", err + } + + return string(response), nil +} + +func (m *Method) FromHexAddress(addr string) (string, error) { + addr = utils.RemoveHexPrefix(addr) + + var response FromHexAddressResponse + err := m.Request(MethodFromHexAddress, FromHexAddressRequest(addr), &response) + if err != nil { + return "", err + } + + return string(response), nil +} + +func (m *Method) GetTransactionReceipt(txHash string) (*GetTransactionReceiptResponse, error) { + var resp *GetTransactionReceiptResponse + err := m.Request(MethodGetTransactionReceipt, GetTransactionReceiptRequest(txHash), &resp) + if err != nil { + return nil, err + } + + return resp, nil +} + +func (m *Method) DecodeRawTransaction(hex string) (*DecodedRawTransactionResponse, error) { + var resp *DecodedRawTransactionResponse + err := m.Request(MethodDecodeRawTransaction, DecodeRawTransactionRequest(hex), &resp) + if err != nil { + return nil, err + } + + return resp, nil +} diff --git a/pkg/qtum/qtum.go b/pkg/qtum/qtum.go index 2c1e3bbe..6f92613c 100644 --- a/pkg/qtum/qtum.go +++ b/pkg/qtum/qtum.go @@ -1,240 +1,13 @@ package qtum -import ( - "fmt" - "math/big" - "strings" - - "github.com/pkg/errors" -) - -const ( - Version = "1.0" -) - -const ( - // FIXME: camel case. MethodGethexaddress -> MethodGetHexAddress - MethodGethexaddress = "gethexaddress" - MethodFromhexaddress = "fromhexaddress" - MethodSendtocontract = "sendtocontract" - MethodGettransactionreceipt = "gettransactionreceipt" - MethodGettransaction = "gettransaction" - MethodCreatecontract = "createcontract" - MethodSendtoaddress = "sendtoaddress" - MethodCallcontract = "callcontract" - MethodDecoderawtransaction = "decoderawtransaction" - MethodGetblockcount = "getblockcount" - MethodGetblockchaininfo = "getblockchaininfo" - MethodSearchlogs = "searchlogs" - MethodWaitforlogs = "waitforlogs" -) - -// FIXME: rename TransactionReceipt -> GetTransactionReceiptResponse -// FIXME: rename all other qtum RPC types -// FIXME: move all RPC types into its own file. -// FIXME: (optional) add RPC examples to each struct -type ( - /* example: - { - "blockhash": "afafafa..." - } - */ - TransactionReceipt struct { - BlockHash string `json:"blockHash"` - BlockNumber uint64 `json:"blockNumber"` - TransactionHash string `json:"transactionHash"` - TransactionIndex uint64 `json:"transactionIndex"` - From string `json:"from"` - To string `json:"to"` - CumulativeGasUsed uint64 `json:"cumulativeGasUsed"` - GasUsed uint64 `json:"gasUsed"` - ContractAddress string `json:"contractAddress"` - Excepted string `json:"excepted"` - Log []Log `json:"log"` - } - - Log struct { - Address string `json:"address"` - Topics []string `json:"topics"` - Data string `json:"data"` - } - - DecodedRawTransactionInV struct { - Txid string `json:"txid"` - Vout int64 `json:"vout"` - ScriptSig struct { - Asm string `json:"asm"` - Hex string `json:"hex"` - } `json:"scriptSig"` - Txinwitness []string `json:"txinwitness"` - Sequence int64 `json:"sequence"` - } - - DecodedRawTransactionOutV struct { - Value float64 `json:"value"` - N int64 `json:"n"` - ScriptPubKey struct { - Asm string `json:"asm"` - Hex string `json:"hex"` - ReqSigs int64 `json:"reqSigs"` - Type string `json:"type"` - Addresses []string `json:"addresses"` - } `json:"scriptPubKey"` - } - - DecodedRawTransaction struct { - Txid string `json:"txid"` - Hash string `json:"hash"` - Size int64 `json:"size"` - Vsize int64 `json:"vsize"` - Version int64 `json:"version"` - Locktime int64 `json:"locktime"` - Vin []*DecodedRawTransactionInV `json:"vin"` - Vout []*DecodedRawTransactionOutV `json:"vout"` - } - - TransactionDetail struct { - Account string `json:"account"` - Address string `json:"address"` - Category string `json:"category"` - Amount float64 `json:"amount"` - Label string `json:"label"` - Vout int64 `json:"vout"` - Fee float64 `json:"fee"` - Abandoned bool `json:"abandoned"` - } - - Transaction struct { - Amount float64 `json:"amount"` - Fee float64 `json:"fee"` - Confirmations int64 `json:"confirmations"` - Blockhash string `json:"blockhash"` - Blockindex int64 `json:"blockindex"` - Blocktime int64 `json:"blocktime"` - Txid string `json:"txid"` - Time int64 `json:"time"` - Timereceived int64 `json:"timereceived"` - Bip125Replaceable string `json:"bip125-replaceable"` - Details []*TransactionDetail `json:"details"` - Hex string `json:"hex"` - } - - // FIXME: extract asm to its own file. btcasm.go - - // ASM is Bitcoin Script extended by qtum to support smart contracts - ASM struct { - VMVersion string - GasLimit string - GasPrice string - Instructor string - } - CallASM struct { - ASM - // FIXME: EncodedABI -> CallData - EncodedABI string - ContractAddress string - } - CreateASM struct { - ASM - // FIXME: EncodedABI -> CallData - EncodedABI string - } - - BlockChainInfo struct { - Bestblockhash string `json:"bestblockhash"` - Bip9Softforks struct { - Csv struct { - Since int64 `json:"since"` - StartTime int64 `json:"startTime"` - Status string `json:"status"` - Timeout int64 `json:"timeout"` - } `json:"csv"` - Segwit struct { - Since int64 `json:"since"` - StartTime int64 `json:"startTime"` - Status string `json:"status"` - Timeout int64 `json:"timeout"` - } `json:"segwit"` - } `json:"bip9_softforks"` - Blocks int64 `json:"blocks"` - Chain string `json:"chain"` - Chainwork string `json:"chainwork"` - Difficulty float64 `json:"difficulty"` - Headers int64 `json:"headers"` - Mediantime int64 `json:"mediantime"` - Pruned bool `json:"pruned"` - Softforks []struct { - ID string `json:"id"` - Reject struct { - Status bool `json:"status"` - } `json:"reject"` - Version int64 `json:"version"` - } `json:"softforks"` - Verificationprogress int64 `json:"verificationprogress"` - } -) - -func ParseCallASM(asm string) (*CallASM, error) { - parts := strings.Split(asm, " ") - if len(parts) < 6 { - // FIXME: typo: sam -> ASM - return nil, errors.New("invalid call sam") - } - - return &CallASM{ - ASM: ASM{ - VMVersion: parts[0], - GasLimit: parts[1], - GasPrice: parts[2], - Instructor: parts[5], - }, - EncodedABI: parts[3], - ContractAddress: parts[4], - }, nil -} - -func ParseCreateASM(asm string) (*CreateASM, error) { - parts := strings.Split(asm, " ") - if len(parts) < 5 { - // FIXME: typo: sam -> ASM - return nil, errors.New("invalid create sam") - } - - return &CreateASM{ - ASM: ASM{ - VMVersion: parts[0], - GasLimit: parts[1], - GasPrice: parts[2], - Instructor: parts[4], - }, - EncodedABI: parts[3], - }, nil -} - -// FIXME: rename GetGasPrice -> GasPrice -func (asm *ASM) GetGasPrice() (*big.Int, error) { - return stringToBigInt(asm.GasPrice) -} - -// FIXME: rename GetGasLimit -> GasLimit -func (asm *ASM) GetGasLimit() (*big.Int, error) { - return stringToBigInt(asm.GasLimit) -} - -func (asm *CreateASM) GetEncodedABI() string { - return asm.EncodedABI -} - -func (asm *CallASM) GetEncodedABI() string { - return asm.EncodedABI +type Qtum struct { + *Client + *Method } -func stringToBigInt(str string) (*big.Int, error) { - var success bool - v := new(big.Int) - if v, success = v.SetString(str, 10); !success { - // FIXME: use errors.Errorf - return nil, errors.New(fmt.Sprintf("Failed to parse big.Int: %s", str)) +func New(c *Client) *Qtum { + return &Qtum{ + Client: c, + Method: &Method{Client: c}, } - return v, nil } diff --git a/pkg/qtum/rpc_types.go b/pkg/qtum/rpc_types.go new file mode 100644 index 00000000..b07d228f --- /dev/null +++ b/pkg/qtum/rpc_types.go @@ -0,0 +1,618 @@ +package qtum + +import ( + "encoding/json" + "errors" + "math/big" + + "github.com/dcb9/janus/pkg/utils" +) + +type ( + Log struct { + Address string `json:"address"` + Topics []string `json:"topics"` + Data string `json:"data"` + } + + /* + { + "chain": "regtest", + "blocks": 4137, + "headers": 4137, + "bestblockhash": "3863e43665ab15af1167df2f30a1c6f658c64704a3a2903bb0c5afde7e5d54ff", + "difficulty": 4.656542373906925e-10, + "mediantime": 1533096368, + "verificationprogress": 1, + "chainwork": "0000000000000000000000000000000000000000000000000000000000002054", + "pruned": false, + "softforks": [ + { + "id": "bip34", + "version": 2, + "reject": { + "status": true + } + }, + { + "id": "bip66", + "version": 3, + "reject": { + "status": true + } + }, + { + "id": "bip65", + "version": 4, + "reject": { + "status": true + } + } + ], + "bip9_softforks": { + "csv": { + "status": "active", + "startTime": 0, + "timeout": 999999999999, + "since": 432 + }, + "segwit": { + "status": "active", + "startTime": 0, + "timeout": 999999999999, + "since": 432 + } + } + } + */ + GetBlockChainInfoResponse struct { + Bestblockhash string `json:"bestblockhash"` + Bip9Softforks struct { + Csv struct { + Since int64 `json:"since"` + StartTime int64 `json:"startTime"` + Status string `json:"status"` + Timeout int64 `json:"timeout"` + } `json:"csv"` + Segwit struct { + Since int64 `json:"since"` + StartTime int64 `json:"startTime"` + Status string `json:"status"` + Timeout int64 `json:"timeout"` + } `json:"segwit"` + } `json:"bip9_softforks"` + Blocks int64 `json:"blocks"` + Chain string `json:"chain"` + Chainwork string `json:"chainwork"` + Difficulty float64 `json:"difficulty"` + Headers int64 `json:"headers"` + Mediantime int64 `json:"mediantime"` + Pruned bool `json:"pruned"` + Softforks []struct { + ID string `json:"id"` + Reject struct { + Status bool `json:"status"` + } `json:"reject"` + Version int64 `json:"version"` + } `json:"softforks"` + Verificationprogress int64 `json:"verificationprogress"` + } +) + +// ========== SendToAddress ============= // + +type ( + SendToAddressRequest struct { + Address string + Amount float64 + SenderAddress string + } + SendToAddressResponse string +) + +func (r *SendToAddressRequest) MarshalJSON() ([]byte, error) { + /* + 1. "address" (string, required) The qtum address to send to. + 2. "amount" (numeric or string, required) The amount in QTUM to send. eg 0.1 + 3. "comment" (string, optional) A comment used to store what the transaction is for. + This is not part of the transaction, just kept in your wallet. + 4. "comment_to" (string, optional) A comment to store the name of the person or organization + to which you're sending the transaction. This is not part of the + transaction, just kept in your wallet. + 5. subtractfeefromamount (boolean, optional, default=false) The fee will be deducted from the amount being sent. + The recipient will receive less qtums than you enter in the amount field. + 6. replaceable (boolean, optional) Allow this transaction to be replaced by a transaction with higher fees via BIP 125 + 7. conf_target (numeric, optional) Confirmation target (in blocks) + 8. "estimate_mode" (string, optional, default=UNSET) The fee estimate mode, must be one of: + "UNSET" + "ECONOMICAL" + "CONSERVATIVE" + 9. "senderaddress" (string, optional) The quantum address that will be used to send money from. + 10."changeToSender" (bool, optional, default=false) Return the change to the sender. + */ + return json.Marshal([]interface{}{ + r.Address, + r.Amount, + "", // comment + "", // comment_to + false, + nil, + nil, + nil, + r.SenderAddress, + true, + }) +} + +// ========== SendToContract ============= // + +type ( + SendToContractRequest struct { + ContractAddress string + Datahex string + Amount float64 + GasLimit *big.Int + GasPrice string + SenderAddress string + } + /* + { + "txid": "6b7f70d8520e1ec87ba7f1ee559b491cc3028b77ae166e789be882b5d370eac9", + "sender": "qTKrsHUrzutdCVu3qi3iV1upzB2QpuRsRb", + "hash160": "6b22910b1e302cf74803ffd1691c2ecb858d3712" + } + */ + SendToContractResponse struct { + Txid string `json:"txid"` + Sender string `json:"sender"` + Hash160 string `json:"hash160"` + } +) + +func (r *SendToContractRequest) MarshalJSON() ([]byte, error) { + /* + 1. "contractaddress" (string, required) The contract address that will receive the funds and data. + 2. "datahex" (string, required) data to send. + 3. "amount" (numeric or string, optional) The amount in QTUM to send. eg 0.1, default: 0 + 4. gasLimit (numeric or string, optional) gasLimit, default: 250000, max: 40000000 + 5. gasPrice (numeric or string, optional) gasPrice Qtum price per gas unit, default: 0.0000004, min:0.0000004 + 6. "senderaddress" (string, optional) The quantum address that will be used as sender. + 7. "broadcast" (bool, optional, default=true) Whether to broadcast the transaction or not. + 8. "changeToSender" (bool, optional, default=true) Return the change to the sender. + */ + + return json.Marshal([]interface{}{ + r.ContractAddress, + r.Datahex, + r.Amount, + r.GasLimit, + r.GasPrice, + r.SenderAddress, + }) +} + +// ========== CreateContract ============= // + +type ( + CreateContractRequest struct { + ByteCode string + GasLimit *big.Int + GasPrice string + } + /* + { + "txid": "d0fe0caa1b798c36da37e9118a06a7d151632d670b82d1c7dc3985577a71880f", + "sender": "qTKrsHUrzutdCVu3qi3iV1upzB2QpuRsRb", + "hash160": "6b22910b1e302cf74803ffd1691c2ecb858d3712", + "address": "c89a5d225f578d84a94741490c1b40889b4f7a00" + } + */ + CreateContractResponse struct { + Txid string `json:"txid"` + Sender string `json:"sender"` + Hash160 string `json:"hash160"` + Address string `json:"address"` + } +) + +func (r *CreateContractRequest) MarshalJSON() ([]byte, error) { + /* + 1. "bytecode" (string, required) contract bytcode. + 2. gasLimit (numeric or string, optional) gasLimit, default: 2500000, max: 40000000 + 3. gasPrice (numeric or string, optional) gasPrice QTUM price per gas unit, default: 0.0000004, min:0.0000004 + 4. "senderaddress" (string, optional) The quantum address that will be used to create the contract. + 5. "broadcast" (bool, optional, default=true) Whether to broadcast the transaction or not. + 6. "changeToSender" (bool, optional, default=true) Return the change to the sender. + */ + return json.Marshal([]interface{}{ + r.ByteCode, + r.GasLimit, + r.GasPrice, + }) +} + +// ========== CallContract ============= // + +type ( + CallContractRequest struct { + From string + To string + Data string + GasLimit *big.Int + } + + /* + { + "address": "1e6f89d7399081b4f8f8aa1ae2805a5efff2f960", + "executionResult": { + "gasUsed": 21678, + "excepted": "None", + "newAddress": "1e6f89d7399081b4f8f8aa1ae2805a5efff2f960", + "output": "0000000000000000000000000000000000000000000000000000000000000001", + "codeDeposit": 0, + "gasRefunded": 0, + "depositSize": 0, + "gasForDeposit": 0 + }, + "transactionReceipt": { + "stateRoot": "d44fc5ad43bae52f01ff7eb4a7bba904ee52aea6c41f337aa29754e57c73fba6", + "gasUsed": 21678, + "bloom": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "log": [] + } + } + */ + CallContractResponse struct { + Address string `json:"address"` + ExecutionResult struct { + GasUsed int `json:"gasUsed"` + Excepted string `json:"excepted"` + NewAddress string `json:"newAddress"` + Output string `json:"output"` + CodeDeposit int `json:"codeDeposit"` + GasRefunded int `json:"gasRefunded"` + DepositSize int `json:"depositSize"` + GasForDeposit int `json:"gasForDeposit"` + } `json:"executionResult"` + TransactionReceipt struct { + StateRoot string `json:"stateRoot"` + GasUsed int `json:"gasUsed"` + Bloom string `json:"bloom"` + Log []interface{} `json:"log"` + } `json:"transactionReceipt"` + } +) + +func (r *CallContractRequest) MarshalJSON() ([]byte, error) { + return json.Marshal([]interface{}{ + utils.RemoveHexPrefix(r.To), + utils.RemoveHexPrefix(r.Data), + r.From, + r.GasLimit, + }) +} + +// ========== FromHexAddress ============= // + +type ( + FromHexAddressRequest string + FromHexAddressResponse string +) + +func (r FromHexAddressRequest) MarshalJSON() ([]byte, error) { + return json.Marshal([]interface{}{ + string(r), + }) +} + +// ========== GetHexAddress ============= // + +type ( + GetHexAddressRequest string + GetHexAddressResponse string +) + +func (r GetHexAddressRequest) MarshalJSON() ([]byte, error) { + return json.Marshal([]interface{}{ + string(r), + }) +} + +// ========== DecodeRawTransaction ============= // +func (r DecodeRawTransactionRequest) MarshalJSON() ([]byte, error) { + return json.Marshal([]interface{}{ + string(r), + }) +} + +type ( + DecodeRawTransactionRequest string + + /* + { + "txid": "d0fe0caa1b798c36da37e9118a06a7d151632d670b82d1c7dc3985577a71880f", + "hash": "d0fe0caa1b798c36da37e9118a06a7d151632d670b82d1c7dc3985577a71880f", + "version": 2, + "size": 552, + "vsize": 552, + "locktime": 608, + "vin": [ + { + "txid": "7f5350dc474f2953a3f30282c1afcad2fb61cdcea5bd949c808ecc6f64ce1503", + "vout": 0, + "scriptSig": { + "asm": "3045022100af4de764705dbd3c0c116d73fe0a2b78c3fab6822096ba2907cfdae2bb28784102206304340a6d260b364ef86d6b19f2b75c5e55b89fb2f93ea72c05e09ee037f60b[ALL] 03520b1500a400483f19b93c4cb277a2f29693ea9d6739daaf6ae6e971d29e3140", + "hex": "483045022100af4de764705dbd3c0c116d73fe0a2b78c3fab6822096ba2907cfdae2bb28784102206304340a6d260b364ef86d6b19f2b75c5e55b89fb2f93ea72c05e09ee037f60b012103520b1500a400483f19b93c4cb277a2f29693ea9d6739daaf6ae6e971d29e3140" + }, + "sequence": 4294967294 + } + ], + "vout": [ + { + "value": 0, + "n": 0, + "scriptPubKey": { + "asm": "4 2500000 40 608060405234801561001057600080fd5b50604051602080610131833981016040525160005560fe806100336000396000f30060806040526004361060485763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166360fe47b18114604d5780636d4ce63c146064575b600080fd5b348015605857600080fd5b5060626004356088565b005b348015606f57600080fd5b50607660cc565b60408051918252519081900360200190f35b600054604080513381526020810192909252805183927f61ec51fdd1350b55fc6e153e60509e993f8dcb537fe4318c45a573243d96cab492908290030190a2600055565b600054905600a165627a7a723058200541c7c0da642ef9004daeb68d281a3c2341e765336f10b4a0ab45dbb7b7f83c00290000000000000000000000000000000000000000000000000000000000000064 OP_CREATE", + "hex": "010403a0252601284d5101608060405234801561001057600080fd5b50604051602080610131833981016040525160005560fe806100336000396000f30060806040526004361060485763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166360fe47b18114604d5780636d4ce63c146064575b600080fd5b348015605857600080fd5b5060626004356088565b005b348015606f57600080fd5b50607660cc565b60408051918252519081900360200190f35b600054604080513381526020810192909252805183927f61ec51fdd1350b55fc6e153e60509e993f8dcb537fe4318c45a573243d96cab492908290030190a2600055565b600054905600a165627a7a723058200541c7c0da642ef9004daeb68d281a3c2341e765336f10b4a0ab45dbb7b7f83c00290000000000000000000000000000000000000000000000000000000000000064c1", + "type": "create" + } + }, + { + "value": 19996.59434, + "n": 1, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 ce7137386121f7531f716d2d4ff36805bc65b3ec OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914ce7137386121f7531f716d2d4ff36805bc65b3ec88ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "qcNwyuvvPhiN4JVgwPp4QWPiK1p7YGvkf1" + ] + } + } + ] + } + */ + DecodedRawTransactionResponse struct { + Txid string `json:"txid"` + Hash string `json:"hash"` + Size int64 `json:"size"` + Vsize int64 `json:"vsize"` + Version int64 `json:"version"` + Locktime int64 `json:"locktime"` + Vin []*DecodedRawTransactionInV `json:"vin"` + Vout []*DecodedRawTransactionOutV `json:"vout"` + } + DecodedRawTransactionInV struct { + Txid string `json:"txid"` + Vout int64 `json:"vout"` + ScriptSig struct { + Asm string `json:"asm"` + Hex string `json:"hex"` + } `json:"scriptSig"` + Txinwitness []string `json:"txinwitness"` + Sequence int64 `json:"sequence"` + } + + DecodedRawTransactionOutV struct { + Value float64 `json:"value"` + N int64 `json:"n"` + ScriptPubKey struct { + Asm string `json:"asm"` + Hex string `json:"hex"` + ReqSigs int64 `json:"reqSigs"` + Type string `json:"type"` + Addresses []string `json:"addresses"` + } `json:"scriptPubKey"` + } +) + +// ========== GetTransactionReceipt ============= // +type ( + GetTransactionReceiptRequest string + GetTransactionReceiptResponse TransactionReceiptStruct + /* + { + "blockHash": "975326b65c20d0b8500f00a59f76b08a98513fff7ce0484382534a47b55f8985", + "blockNumber": 4063, + "transactionHash": "c1816e5fbdd4d1cc62394be83c7c7130ccd2aadefcd91e789c1a0b33ec093fef", + "transactionIndex": 2, + "from": "6b22910b1e302cf74803ffd1691c2ecb858d3712", + "to": "db46f738bf32cdafb9a4a70eb8b44c76646bcaf0", + "cumulativeGasUsed": 68572, + "gasUsed": 68572, + "contractAddress": "db46f738bf32cdafb9a4a70eb8b44c76646bcaf0", + "excepted": "None", + "log": [ + { + "address": "db46f738bf32cdafb9a4a70eb8b44c76646bcaf0", + "topics": [ + "0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885", + "0000000000000000000000006b22910b1e302cf74803ffd1691c2ecb858d3712" + ], + "data": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "address": "db46f738bf32cdafb9a4a70eb8b44c76646bcaf0", + "topics": [ + "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000006b22910b1e302cf74803ffd1691c2ecb858d3712" + ], + "data": "0000000000000000000000000000000000000000000000000000000000000001" + } + ] + } + */ + TransactionReceiptStruct struct { + BlockHash string `json:"blockHash"` + BlockNumber uint64 `json:"blockNumber"` + TransactionHash string `json:"transactionHash"` + TransactionIndex uint64 `json:"transactionIndex"` + From string `json:"from"` + To string `json:"to"` + CumulativeGasUsed uint64 `json:"cumulativeGasUsed"` + GasUsed uint64 `json:"gasUsed"` + ContractAddress string `json:"contractAddress"` + Excepted string `json:"excepted"` + Log []Log `json:"log"` + } +) + +func (r GetTransactionReceiptRequest) MarshalJSON() ([]byte, error) { + /* + 1. "hash" (string, required) The transaction hash + */ + return json.Marshal([]interface{}{ + string(r), + }) +} + +var EmptyResponseErr = errors.New("result is empty") + +func (r *GetTransactionReceiptResponse) UnmarshalJSON(data []byte) error { + type Response GetTransactionReceiptResponse + var resp []Response + if err := json.Unmarshal(data, &resp); err != nil { + return err + } + + if len(resp) == 0 { + return EmptyResponseErr + } + + *r = GetTransactionReceiptResponse(resp[0]) + + return nil +} + +// ========== GetBlockCount ============= // + +type ( + GetBlockCountResponse big.Int +) + +func (r *GetBlockCountResponse) UnmarshalJSON(data []byte) error { + var i big.Int + err := json.Unmarshal(data, &i) + if err != nil { + return err + } + + *r = GetBlockCountResponse(i) + return nil +} + +// ========== GetTransaction ============= // + +type ( + GetTransactionRequest struct { + Txid string + } + + /* + { + "amount": 0, + "fee": -0.2012, + "confirmations": 2, + "blockhash": "ea26fd59a2145dcecd0e2f81b701019b51ca754b6c782114825798973d8187d6", + "blockindex": 2, + "blocktime": 1533092896, + "txid": "11e97fa5877c5df349934bafc02da6218038a427e8ed081f048626fa6eb523f5", + "walletconflicts": [], + "time": 1533092879, + "timereceived": 1533092879, + "bip125-replaceable": "no", + "details": [ + { + "account": "", + "category": "send", + "amount": 0, + "vout": 0, + "fee": -0.2012, + "abandoned": false + } + ], + "hex": "020000000159c0514feea50f915854d9ec45bc6458bb14419c78b17e7be3f7fd5f563475b5010000006a473044022072d64a1f4ea2d54b7b05050fc853ab192c91cc5ca17e23007867f92f2ab59d9202202b8c9ab9348c8edbb3b98b1788382c8f37642ec9bd6a4429817ab79927319200012103520b1500a400483f19b93c4cb277a2f29693ea9d6739daaf6ae6e971d29e3140feffffff02000000000000000063010403400d0301644440c10f190000000000000000000000006b22910b1e302cf74803ffd1691c2ecb858d3712000000000000000000000000000000000000000000000000000000000000000a14be528c8378ff082e4ba43cb1baa363dbf3f577bfc260e66272970100001976a9146b22910b1e302cf74803ffd1691c2ecb858d371288acb00f0000" + } + */ + GetTransactionResponse struct { + Amount float64 `json:"amount"` + Fee float64 `json:"fee"` + Confirmations int64 `json:"confirmations"` + Blockhash string `json:"blockhash"` + Blockindex int64 `json:"blockindex"` + Blocktime int64 `json:"blocktime"` + Txid string `json:"txid"` + Time int64 `json:"time"` + Timereceived int64 `json:"timereceived"` + Bip125Replaceable string `json:"bip125-replaceable"` + Details []*TransactionDetail `json:"details"` + Hex string `json:"hex"` + } + TransactionDetail struct { + Account string `json:"account"` + Address string `json:"address"` + Category string `json:"category"` + Amount float64 `json:"amount"` + Label string `json:"label"` + Vout int64 `json:"vout"` + Fee float64 `json:"fee"` + Abandoned bool `json:"abandoned"` + } +) + +func (r *GetTransactionRequest) MarshalJSON() ([]byte, error) { + /* + 1. "txid" (string, required) The transaction id + 2. "include_watchonly" (bool, optional, default=false) Whether to include watch-only addresses in balance calculation and details[] + 3. "waitconf" (int, optional, default=0) Wait for enough confirmations before returning + */ + return json.Marshal([]interface{}{ + r.Txid, + }) +} + +func (r *GetTransactionResponse) UnmarshalJSON(data []byte) error { + if string(data) == "[]" { + return EmptyResponseErr + } + type Response GetTransactionResponse + var resp Response + if err := json.Unmarshal(data, &resp); err != nil { + return err + } + + *r = GetTransactionResponse(resp) + + return nil +} + +// ========== SearchLogs ============= // + +type ( + SearchLogsRequest struct { + FromBlock *big.Int + ToBlock *big.Int + Addresses []string + } + + SearchLogsResponse []TransactionReceiptStruct +) + +func (r *SearchLogsRequest) MarshalJSON() ([]byte, error) { + /* + 1. "fromBlock" (numeric, required) The number of the earliest block (latest may be given to mean the most recent block). + 2. "toBlock" (string, required) The number of the latest block (-1 may be given to mean the most recent block). + 3. "address" (string, optional) An address or a list of addresses to only get logs from particular account(s). + 4. "topics" (string, optional) An array of values from which at least one must appear in the log entries. The order is important, if you want to leave topics out use null, e.g. ["null", "0x00..."]. + 5. "minconf" (uint, optional, default=0) Minimal number of confirmations before a log is returned + */ + return json.Marshal([]interface{}{ + r.FromBlock, + r.ToBlock, + map[string][]string{ + "addresses": r.Addresses, + }, + }) +} diff --git a/pkg/rpc/jsonrpc.go b/pkg/rpc/jsonrpc.go deleted file mode 100644 index f3a9954a..00000000 --- a/pkg/rpc/jsonrpc.go +++ /dev/null @@ -1,46 +0,0 @@ -package rpc - -import ( - "encoding/json" - "fmt" -) - -// FIXME: move to qtum package -const ( - ErrInvalid = 150 - ErrUnknownOperation = 151 -) - -// FIXME: rename package to jsonrpc -// FIXME: remove JSONRPC prefix for JSONRPCRequest, JSONRPCResult -// FIXME: this package seems kinda pointless - -type JSONRPCRequest struct { - JSONRPC string `json:"jsonrpc"` - Method string `json:"method"` - ID json.RawMessage `json:"id"` - Params json.RawMessage `json:"params"` -} - -type JSONRPCResult struct { - JSONRPC string `json:"jsonrpc"` - RawResult json.RawMessage `json:"result"` - Error *JSONRPCError `json:"error,omitempty"` - ID json.RawMessage `json:"id"` -} - -type SuccessJSONRPCResult struct { - JSONRPC string `json:"jsonrpc"` - RawResult json.RawMessage `json:"result"` - ID json.RawMessage `json:"id"` -} - -// FIXME: move this to qtum package -type JSONRPCError struct { - Code int `json:"code"` - Message string `json:"message"` -} - -func (err *JSONRPCError) Error() string { - return fmt.Sprintf("[code: %d] %s", err.Code, err.Message) -} diff --git a/pkg/server/handler.go b/pkg/server/handler.go index 0d017245..d1b2f7c6 100644 --- a/pkg/server/handler.go +++ b/pkg/server/handler.go @@ -1,93 +1,44 @@ package server import ( - "encoding/json" "log" - "github.com/dcb9/janus/pkg/rpc" - "github.com/dcb9/janus/pkg/transformer" + "github.com/dcb9/janus/pkg/eth" "github.com/go-kit/kit/log/level" "github.com/labstack/echo" "github.com/pkg/errors" ) -// FIXME: refactor this method. move the `request -> transform -> response` to transformer package -func httpHandler(c *myCtx) (interface{}, error) { - rpcReq := c.rpcReq - - // FIXME: move to transformer - switch rpcReq.Method { - case "personal_unlockAccount": - return true, nil +func httpHandler(c echo.Context) error { + myctx := c.Get("myctx") + cc, ok := myctx.(*myCtx) + if !ok { + return errors.New("Could not find myctx") } - // FIXME: - // proxyRes, err = TransformRequest.Proxy(rpcReq) - // output proxyRes as JSON. done. - - level.Info(c.logger).Log("msg", "before transform request", "method", rpcReq.Method, "params", []byte(rpcReq.Params)) - responseTransformer, err := c.server.transformerManager.TransformRequest(rpcReq) - + level.Debug(cc.logger).Log("msg", "before call transformer#Transform") + result, err := cc.transformer.Transform(cc.rpcReq) + level.Debug(cc.logger).Log("msg", "after call transformer#Transform") if err != nil { - level.Error(c.logger).Log("transformErr", err) - return nil, err + return err } - level.Info(c.logger).Log("msg", "after transform request", "method", rpcReq.Method, "params", []byte(rpcReq.Params)) - - result, err := c.server.qtumClient.Request(rpcReq) - if err != nil { - return nil, errors.Wrap(err, "qtum client request") - } - if result.RawResult == nil { - return nil, errors.New("server response result.RawResult is nil") - } - - if responseTransformer != nil { - level.Info(c.logger).Log("msg", "before transform response", "rawResult", []byte(result.RawResult)) - newResult, err := responseTransformer(result.RawResult) - if err != nil { - level.Error(c.logger).Log("msg", "transform response", "err", err) - return nil, err - } - if result.RawResult, err = json.Marshal(newResult); err != nil { - return nil, err - } - level.Info(c.logger).Log("msg", "after transform response", "rawResult", []byte(result.RawResult)) - } - - return result.RawResult, nil + return cc.JSONRPCResult(result) } func errorHandler(err error, c echo.Context) { myctx := c.Get("myctx") cc, ok := myctx.(*myCtx) if ok { - err := errors.Cause(err) - if err == transformer.UnmarshalRequestErr { - if err := cc.JSONRPCError(&rpc.JSONRPCError{ - Code: rpc.ErrInvalid, - Message: "Input is invalid", - }); err != nil { - level.Error(cc.logger).Log("msg", "reply to client error", "err", err) - } - return - } - - switch err.(type) { - case *rpc.JSONRPCError: - if err := cc.JSONRPCError(err.(*rpc.JSONRPCError)); err != nil { - level.Error(cc.logger).Log("msg", "reply to client error", "err", err) - } - return + err1 := errors.Cause(err) + if err != err1 { + level.Error(cc.logger).Log("err", err.Error()) } + cc.JSONRPCError(ð.JSONRPCError{ + Code: 100, + Message: err1.Error(), + }) - if err := cc.JSONRPCError(&rpc.JSONRPCError{ - Code: rpc.ErrInvalid, - Message: err.Error(), - }); err != nil { - level.Error(cc.logger).Log("msg", "reply to client error", "err", err) - } return } diff --git a/pkg/server/myctx.go b/pkg/server/myctx.go index cfc7d788..f4d4b503 100644 --- a/pkg/server/myctx.go +++ b/pkg/server/myctx.go @@ -5,53 +5,42 @@ import ( "net/http" "github.com/dcb9/janus/pkg/eth" - "github.com/dcb9/janus/pkg/rpc" + "github.com/dcb9/janus/pkg/transformer" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/labstack/echo" - "github.com/pkg/errors" ) type myCtx struct { echo.Context - rpcReq *rpc.JSONRPCRequest - logger log.Logger - server *Server + rpcReq *eth.JSONRPCRequest + logger log.Logger + transformer *transformer.Transformer } func (c *myCtx) JSONRPCResult(result interface{}) error { - bytes, err := json.Marshal(result) + response, err := eth.NewJSONRPCResult(c.rpcReq.ID, result) if err != nil { - return errors.Wrap(err, "myCtx#JSONRPCResult") - } - return c.Context.JSON(http.StatusOK, eth.NewJSONRPCResult(c.rpcReq.ID, bytes, nil)) -} - -func (c *myCtx) JSONRPCError(err *rpc.JSONRPCError) error { - resp := eth.NewJSONRPCResult(c.rpcReq.ID, nil, err) - respBytes, marshalErr := json.Marshal(resp) - if marshalErr != nil { - return marshalErr + return err } - level.Error(c.logger).Log("component", "myCtx#JSONRPCError", "resp", respBytes) - return c.Context.JSON(http.StatusInternalServerError, resp) + return c.JSON(http.StatusOK, response) } -func (s *Server) myCtxHandler(h func(*myCtx) (result interface{}, err error)) echo.HandlerFunc { - return func(c echo.Context) error { - var rpcReq rpc.JSONRPCRequest - if err := c.Bind(&rpcReq); err != nil { - return err - } - - cc := c.(*myCtx) - cc.rpcReq = &rpcReq +func (c *myCtx) JSONRPCError(err *eth.JSONRPCError) { + var id json.RawMessage + if c.rpcReq != nil && c.rpcReq.ID != nil { + id = c.rpcReq.ID + } + resp := ð.JSONRPCResult{ + ID: id, + Error: err, + JSONRPC: eth.RPCVersion, + } - result, err := h(cc) - if err != nil { - return err + if !c.Response().Committed { + if err := c.JSON(http.StatusInternalServerError, resp); err != nil { + level.Error(c.logger).Log("replyToClientErr", err.Error()) } - return cc.JSONRPCResult(result) } } diff --git a/pkg/server/options.go b/pkg/server/options.go deleted file mode 100644 index 99c0fef4..00000000 --- a/pkg/server/options.go +++ /dev/null @@ -1,53 +0,0 @@ -package server - -import ( - "errors" - "fmt" - "net/url" - - "github.com/go-kit/kit/log" -) - -type Option func(*Server) error - -func SetLogger(l log.Logger) Option { - return func(p *Server) error { - p.logger = l - return nil - } -} - -func SetDebug(debug bool) Option { - return func(p *Server) error { - p.debug = debug - return nil - } -} - -func setAddress(addr string) Option { - return func(p *Server) error { - p.address = addr - return nil - } -} - -func setQtumRPC(r string) Option { - return func(p *Server) error { - if r == "" { - return errors.New("Please set QTUM_RPC to qtumd's RPC URL") - } - - qtumRPC, err := url.Parse(r) - if err != nil { - return errors.New(fmt.Sprintf("QTUM_RPC URL: %s", r)) - } - - if qtumRPC.User == nil { - return errors.New(fmt.Sprintf("QTUM_RPC URL (must specify user & password): %s", r)) - } - - p.qtumRPC = qtumRPC - - return nil - } -} diff --git a/pkg/server/server.go b/pkg/server/server.go index fc1e43df..fe2af3bc 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -1,33 +1,36 @@ package server import ( - "net/url" - + "github.com/dcb9/janus/pkg/eth" "github.com/dcb9/janus/pkg/qtum" "github.com/dcb9/janus/pkg/transformer" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/labstack/echo" + "github.com/labstack/echo/middleware" ) type Server struct { - qtumRPC *url.URL - address string - transformerManager *transformer.Manager - qtumClient *qtum.Client - logger log.Logger - debug bool - echo *echo.Echo + address string + transformer *transformer.Transformer + qtumRPCClient *qtum.Qtum + logger log.Logger + debug bool + echo *echo.Echo } -// FIXME: define a StartServer/main function to do dependency injection. Constructor should be simple. - -func New(qtumRPC string, addr string, opts ...Option) (*Server, error) { - opts = append(opts, setQtumRPC(qtumRPC), setAddress(addr)) - +func New( + qtumRPCClient *qtum.Qtum, + transformer *transformer.Transformer, + addr string, + opts ...Option, +) (*Server, error) { p := &Server{ - logger: log.NewNopLogger(), - echo: echo.New(), + logger: log.NewNopLogger(), + echo: echo.New(), + address: addr, + qtumRPCClient: qtumRPCClient, + transformer: transformer, } var err error @@ -37,47 +40,61 @@ func New(qtumRPC string, addr string, opts ...Option) (*Server, error) { } } - // FIXME: dependency injection: pass client an transformer into constructor as parameters - - p.qtumClient, err = qtum.NewClient( - qtumRPC, - qtum.SetLogger(p.logger), - qtum.SetDebug(p.debug), - ) - if err != nil { - return nil, err - } - - p.transformerManager, err = transformer.NewManager( - p.qtumClient, - transformer.SetLogger(p.logger), - transformer.SetDebug(p.debug), - ) - if err != nil { - return nil, err - } - return p, nil } func (s *Server) Start() error { e := s.echo + e.HTTPErrorHandler = errorHandler + e.Use(middleware.BodyDump(func(c echo.Context, req []byte, resp []byte) { + myctx := c.Get("myctx") + cc, ok := myctx.(*myCtx) + if !ok { + return + } + + level.Debug(cc.logger).Log("msg", "body dump", "reqBody", req, "respBody", resp) + })) + e.Use(func(h echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { cc := &myCtx{ - Context: c, - server: s, - logger: s.logger, + Context: c, + logger: s.logger, + transformer: s.transformer, } + c.Set("myctx", cc) - return h(cc) + + var rpcReq *eth.JSONRPCRequest + if err := c.Bind(&rpcReq); err != nil { + return err + } + + cc.rpcReq = rpcReq + + return h(c) } }) - e.HideBanner = true - e.POST("/*", s.myCtxHandler(httpHandler)) - e.HTTPErrorHandler = errorHandler + e.POST("/*", httpHandler) - level.Warn(s.logger).Log("listen", s.address, "qtum_rpc", s.qtumRPC, "msg", "proxy started") + level.Warn(s.logger).Log("listen", s.address, "qtum_rpc", s.qtumRPCClient.URL, "msg", "proxy started") return e.Start(s.address) } + +type Option func(*Server) error + +func SetLogger(l log.Logger) Option { + return func(p *Server) error { + p.logger = l + return nil + } +} + +func SetDebug(debug bool) Option { + return func(p *Server) error { + p.debug = debug + return nil + } +} diff --git a/pkg/transformer/callcontract.go b/pkg/transformer/callcontract.go deleted file mode 100644 index 098a2d43..00000000 --- a/pkg/transformer/callcontract.go +++ /dev/null @@ -1,108 +0,0 @@ -package transformer - -import ( - "encoding/json" - "errors" - - "github.com/bitly/go-simplejson" - "github.com/dcb9/janus/pkg/eth" - "github.com/dcb9/janus/pkg/qtum" - "github.com/dcb9/janus/pkg/rpc" -) - -// method: eth_call -// -// eth request -> qtum request -// call qtum rpc -// qtum reponse -> eth response - -func (m *Manager) proxyRPC(req *rpc.JSONRPCRequest) (interface{}, error) { - switch req.Method { - case "eth_call": - var req eth.CallRequest - err := unmarshalRequest(req.Params, &req) - if err != nil { - return nil, err - } - return m.proxyETHCall(req) - default: - return errors.New("unsupported method") - } -} - -type Proxy interface { - request(ethreq interface{}) (interface{}, error) -} - -// FIXME: rename file to eth_call.go -// proxy eth_call - -type ProxyETHCall struct{} - -func (p *ProxyETHCall) request(rawreq *rpc.JSONRPCRequest) (interface{}, error) { - var req eth.CallRequest - err := unmarshalRequest(rawreq.Params, &req) - if err != nil { - return nil, err - } - return p.proxyInternal(req) -} - -func (p *ProxyETHCall) requestInternal(ethreq *eth.CallRequest) (*eth.CallResponse, error) { - // eth req -> qtum req - qtumreq, err = p.ToRequest(ethreq) - - var qtumres qtum.CallContract - err = p.rpc.Request(qtumreq, &qtumres) - - // qtum res -> eth res - ethres, err = p.ToResponse(ethreq) - - return ethres, err -} - -func (p *ProxyETHCall) ToRequest(ethreq *eth.CallRequest) (*qtum.CallContract, error) { -} - -func (p *ProxyETHCall) ToResponse(res *qtum.CallResponse) (*eth.CallResponse, error) { -} - -func (m *Manager) Call(req *rpc.JSONRPCRequest) (ResponseTransformerFunc, error) { - var params []json.RawMessage - if err := unmarshalRequest(req.Params, ¶ms); err != nil { - return nil, err - } - if len(params) == 0 { - return nil, errors.New("params must be set") - } - - // FIXME: rename to eth.CallRequest - var tx eth.TransactionCallReq - if err := unmarshalRequest(params[0], &tx); err != nil { - return nil, err - } - - //Qtum RPC - // callcontract "address" "data" ( address ) - // - // Argument: - // 1. "address" (string, required) The account address - // 2. "data" (string, required) The data hex string - // 3. address (string, optional) The sender address hex string - // 4. gasLimit (string, optional) The gas limit for executing the contract - - return m.CallcontractResp, nil -} - -func (m *Manager) CallcontractResp(result json.RawMessage) (interface{}, error) { - sj, err := simplejson.NewJson(result) - if err != nil { - return nil, err - } - output, err := sj.Get("executionResult").Get("output").String() - if err != nil { - return nil, err - } - - return AddHexPrefix(output), nil -} diff --git a/pkg/transformer/createcontract.go b/pkg/transformer/createcontract.go deleted file mode 100644 index c4564e4e..00000000 --- a/pkg/transformer/createcontract.go +++ /dev/null @@ -1,86 +0,0 @@ -package transformer - -import ( - "encoding/json" - - "github.com/dcb9/janus/pkg/eth" - "github.com/dcb9/janus/pkg/qtum" - "github.com/dcb9/janus/pkg/rpc" -) - -func (m *Manager) createcontract(req *rpc.JSONRPCRequest, tx *eth.TransactionReq) error { - if tx.Value != "" && tx.Value != "0x0" { - return &rpc.JSONRPCError{ - Code: rpc.ErrInvalid, - Message: "value must be empty", - } - } - - gasLimit, gasPrice, err := EthGasToQtum(tx) - if err != nil { - return err - } - params := []interface{}{ - RemoveHexPrefix(tx.Data), - gasLimit, - gasPrice, - } - - if tx.From != "" { - from := tx.From - if IsEthHexAddress(from) { - from, err = m.qtumClient.FromHexAddress(RemoveHexPrefix(from)) - if err != nil { - return err - } - } - - params = append(params, from) - } - - newParams, err := json.Marshal(params) - if err != nil { - return err - } - - req.Params = newParams - req.Method = qtum.MethodCreatecontract - - return nil -} - -// Eth RPC -// params: [{ -// "from": "0xb60e8dd61c5d32be8058bb8eb970870f07233155", -// "to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567", -// "gas": "0x76c0", // 30400 -// "gasPrice": "0x9184e72a000", // 10000000000000 -// "value": "", -// "data": "0xd46e...675" -// }] - -//Qtum RPC -// createcontract "bytecode" (gaslimit gasprice "senderaddress" broadcast) -// Create a contract with bytcode. -// -//Arguments: -// 1. "bytecode" (string, required) contract bytcode. -// 2. gasLimit (numeric or string, optional) gasLimit, default: 2500000, max: 40000000 -// 3. gasPrice (numeric or string, optional) gasPrice QTUM price per gas unit, default: 0.0000004, min:0.0000004 -// 4. "senderaddress" (string, optional) The quantum address that will be used to create the contract. -// 5. "broadcast" (bool, optional, default=true) Whether to broadcast the transaction or not. -// 6. "changeToSender" (bool, optional, default=true) Return the change to the sender. -// -//Result: -// [ -// { -// "txid" : (string) The transaction id. -// "sender" : (string) QTUM address of the sender. -// "hash160" : (string) ripemd-160 hash of the sender. -// "address" : (string) expected contract address. -// } -// ] -// -//Examples: -// > qtum-cli createcontract "60606040525b33600060006101000a81548173ffffffffffffffffffffffffffffffffffffffff02191690836c010000000000000000000000009081020402179055506103786001600050819055505b600c80605b6000396000f360606040526008565b600256" -// > qtum-cli createcontract "60606040525b33600060006101000a81548173ffffffffffffffffffffffffffffffffffffffff02191690836c010000000000000000000000009081020402179055506103786001600050819055505b600c80605b6000396000f360606040526008565b600256" 6000000 0.0000004 "QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd" true diff --git a/pkg/transformer/eth_blockNumber.go b/pkg/transformer/eth_blockNumber.go new file mode 100644 index 00000000..0043bafb --- /dev/null +++ b/pkg/transformer/eth_blockNumber.go @@ -0,0 +1,39 @@ +package transformer + +import ( + "math/big" + + "github.com/dcb9/janus/pkg/eth" + "github.com/dcb9/janus/pkg/qtum" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// ProxyETHBlockNumber implements ETHProxy +type ProxyETHBlockNumber struct { + *qtum.Qtum +} + +func (p *ProxyETHBlockNumber) Method() string { + return "eth_blockNumber" +} + +func (p *ProxyETHBlockNumber) Request(_ *eth.JSONRPCRequest) (interface{}, error) { + return p.request() +} + +func (p *ProxyETHBlockNumber) request() (*eth.BlockNumberResponse, error) { + var qtumresp *qtum.GetBlockCountResponse + if err := p.Qtum.Request(qtum.MethodGetBlockCount, nil, &qtumresp); err != nil { + return nil, err + } + + // qtum res -> eth res + return p.ToResponse(qtumresp), nil +} + +func (p *ProxyETHBlockNumber) ToResponse(qtumresp *qtum.GetBlockCountResponse) *eth.BlockNumberResponse { + v := big.Int(*qtumresp) + hexVal := hexutil.EncodeBig(&v) + ethresp := eth.BlockNumberResponse(hexVal) + return ðresp +} diff --git a/pkg/transformer/eth_call.go b/pkg/transformer/eth_call.go new file mode 100644 index 00000000..7bea6529 --- /dev/null +++ b/pkg/transformer/eth_call.go @@ -0,0 +1,70 @@ +package transformer + +import ( + "github.com/dcb9/janus/pkg/eth" + "github.com/dcb9/janus/pkg/qtum" + "github.com/dcb9/janus/pkg/utils" +) + +// ProxyEthCall implements ETHProxy +type ProxyETHCall struct { + *qtum.Qtum +} + +func (p *ProxyETHCall) Method() string { + return "eth_call" +} + +func (p *ProxyETHCall) Request(rawreq *eth.JSONRPCRequest) (interface{}, error) { + var req eth.CallRequest + if err := unmarshalRequest(rawreq.Params, &req); err != nil { + return nil, err + } + + return p.request(&req) +} + +func (p *ProxyETHCall) request(ethreq *eth.CallRequest) (*eth.CallResponse, error) { + // eth req -> qtum req + params, err := p.ToRequest(ethreq) + if err != nil { + return nil, err + } + + var qtumresp *qtum.CallContractResponse + if err = p.Qtum.Request(qtum.MethodCallContract, params, &qtumresp); err != nil { + return nil, err + } + + // qtum res -> eth res + return p.ToResponse(qtumresp), nil +} + +func (p *ProxyETHCall) ToRequest(ethreq *eth.CallRequest) (*qtum.CallContractRequest, error) { + gasLimit, _, err := EthGasToQtum(ethreq) + if err != nil { + return nil, err + } + + from := ethreq.From + + if utils.IsEthHexAddress(from) { + from, err = p.FromHexAddress(from) + if err != nil { + return nil, err + } + } + + return &qtum.CallContractRequest{ + To: ethreq.To, + From: ethreq.From, + Data: ethreq.Data, + GasLimit: gasLimit, + }, nil +} + +func (p *ProxyETHCall) ToResponse(ethresp *qtum.CallContractResponse) *eth.CallResponse { + data := utils.AddHexPrefix(ethresp.ExecutionResult.Output) + qtumresp := eth.CallResponse(data) + return &qtumresp +} diff --git a/pkg/transformer/eth_getLogs.go b/pkg/transformer/eth_getLogs.go new file mode 100644 index 00000000..90fc2b75 --- /dev/null +++ b/pkg/transformer/eth_getLogs.go @@ -0,0 +1,145 @@ +package transformer + +import ( + "encoding/json" + "math/big" + + "github.com/dcb9/janus/pkg/eth" + "github.com/dcb9/janus/pkg/qtum" + "github.com/dcb9/janus/pkg/utils" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/pkg/errors" +) + +// ProxyETHGetLogs implements ETHProxy +type ProxyETHGetLogs struct { + *qtum.Qtum +} + +func (p *ProxyETHGetLogs) Method() string { + return "eth_getLogs" +} + +func (p *ProxyETHGetLogs) Request(rawreq *eth.JSONRPCRequest) (interface{}, error) { + var req eth.GetLogsRequest + if err := unmarshalRequest(rawreq.Params, &req); err != nil { + return nil, err + } + + if len(req.Topics) != 0 { + return nil, errors.New("topics is not supported yet") + } + + qtumreq, err := p.ToRequest(&req) + if err != nil { + return nil, err + } + + return p.request(qtumreq) +} + +func (p *ProxyETHGetLogs) request(req *qtum.SearchLogsRequest) (*eth.GetLogsResponse, error) { + var receipts qtum.SearchLogsResponse + if err := p.Qtum.Request(qtum.MethodSearchLogs, req, &receipts); err != nil { + return nil, err + } + + logs := make([]eth.Log, 0) + for _, receipt := range receipts { + r := qtum.TransactionReceiptStruct(receipt) + logs = append(logs, getEthLogs(&r)...) + } + + resp := eth.GetLogsResponse(logs) + return &resp, nil +} + +func (p *ProxyETHGetLogs) ToRequest(ethreq *eth.GetLogsRequest) (*qtum.SearchLogsRequest, error) { + from, err := getQtumBlockNumber(ethreq.FromBlock, 0) + if err != nil { + return nil, err + } + + to, err := getQtumBlockNumber(ethreq.ToBlock, -1) + if err != nil { + return nil, err + } + + var addresses []string + if ethreq.Address != nil { + if isString(ethreq.Address) { + var addr string + if err = json.Unmarshal(ethreq.Address, &addr); err != nil { + return nil, err + } + addresses = append(addresses, addr) + } else { + if err = json.Unmarshal(ethreq.Address, &addresses); err != nil { + return nil, err + } + } + for i, _ := range addresses { + addresses[i] = utils.RemoveHexPrefix(addresses[i]) + } + } + + return &qtum.SearchLogsRequest{ + Addresses: addresses, + FromBlock: from, + ToBlock: to, + }, nil +} + +func getQtumBlockNumber(ethBlock json.RawMessage, defaultVal int64) (*big.Int, error) { + if ethBlock == nil { + return big.NewInt(defaultVal), nil + } + + if isString(ethBlock) { + var ethBlockStr string + if err := json.Unmarshal(ethBlock, ðBlockStr); err != nil { + return nil, err + } + + switch ethBlockStr { + case "latest": + return big.NewInt(-1), nil + case "pending", "earliest": + return nil, errors.New(`tags, "pending" and "earliest", are unsupported`) + default: + return utils.DecodeBig(ethBlockStr) + } + } + + var b int64 + if err := json.Unmarshal(ethBlock, &b); err != nil { + return nil, err + } + + return big.NewInt(b), nil +} + +func isString(v json.RawMessage) bool { + return v[0] == '"' +} + +func getEthLogs(receipt *qtum.TransactionReceiptStruct) []eth.Log { + logs := make([]eth.Log, 0, len(receipt.Log)) + for index, log := range receipt.Log { + topics := make([]string, 0, len(log.Topics)) + for _, topic := range log.Topics { + topics = append(topics, utils.AddHexPrefix(topic)) + } + logs = append(logs, eth.Log{ + TransactionHash: utils.AddHexPrefix(receipt.TransactionHash), + TransactionIndex: hexutil.EncodeUint64(receipt.TransactionIndex), + BlockHash: utils.AddHexPrefix(receipt.BlockHash), + BlockNumber: hexutil.EncodeUint64(receipt.BlockNumber), + Data: utils.AddHexPrefix(log.Data), + Address: utils.AddHexPrefix(log.Address), + Topics: topics, + LogIndex: hexutil.EncodeUint64(uint64(index)), + }) + } + return logs +} diff --git a/pkg/transformer/eth_getTransactionByHash.go b/pkg/transformer/eth_getTransactionByHash.go new file mode 100644 index 00000000..53fd3fa7 --- /dev/null +++ b/pkg/transformer/eth_getTransactionByHash.go @@ -0,0 +1,124 @@ +package transformer + +import ( + "math/big" + + "fmt" + + "github.com/dcb9/janus/pkg/eth" + "github.com/dcb9/janus/pkg/qtum" + "github.com/dcb9/janus/pkg/utils" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/pkg/errors" +) + +// ProxyETHGetTransactionByHash implements ETHProxy +type ProxyETHGetTransactionByHash struct { + *qtum.Qtum +} + +func (p *ProxyETHGetTransactionByHash) Method() string { + return "eth_getTransactionByHash" +} + +func (p *ProxyETHGetTransactionByHash) Request(rawreq *eth.JSONRPCRequest) (interface{}, error) { + var req eth.GetTransactionByHashRequest + if err := unmarshalRequest(rawreq.Params, &req); err != nil { + return nil, err + } + + qtumreq := p.ToRequest(&req) + + return p.request(qtumreq) +} + +func (p *ProxyETHGetTransactionByHash) request(req *qtum.GetTransactionRequest) (*eth.GetTransactionByHashResponse, error) { + var tx *qtum.GetTransactionResponse + if err := p.Qtum.Request(qtum.MethodGetTransaction, req, &tx); err != nil { + fmt.Println("err", err.Error()) + if err == qtum.EmptyResponseErr { + return nil, nil + } + + return nil, err + } + + ethVal, err := QtumAmountToEthValue(tx.Amount) + if err != nil { + return nil, err + } + + decodedRawTx, err := p.Qtum.DecodeRawTransaction(tx.Hex) + if err != nil { + return nil, errors.Wrap(err, "Qtum#DecodeRawTransaction") + } + + var gas, gasPrice, input string + type asmWithGasGasPriceEncodedABI interface { + CallData() string + GasPrice() (*big.Int, error) + GasLimit() (*big.Int, error) + } + + var asm asmWithGasGasPriceEncodedABI + for _, out := range decodedRawTx.Vout { + switch out.ScriptPubKey.Type { + case "call": + if asm, err = qtum.ParseCallASM(out.ScriptPubKey.Asm); err != nil { + return nil, err + } + case "create": + if asm, err = qtum.ParseCreateASM(out.ScriptPubKey.Asm); err != nil { + return nil, err + } + default: + continue + } + break + } + + if asm != nil { + input = utils.AddHexPrefix(asm.CallData()) + gasLimitBigInt, err := asm.GasLimit() + if err != nil { + return nil, err + } + gasPriceBigInt, err := asm.GasPrice() + if err != nil { + return nil, err + } + gas = hexutil.EncodeBig(gasLimitBigInt) + gasPrice = hexutil.EncodeBig(gasPriceBigInt) + } + + ethTxResp := eth.GetTransactionByHashResponse{ + Hash: utils.AddHexPrefix(tx.Txid), + BlockHash: utils.AddHexPrefix(tx.Blockhash), + Nonce: "", + Value: ethVal, + Input: input, + Gas: gas, + GasPrice: gasPrice, + } + + if asm != nil { + receipt, err := p.Qtum.GetTransactionReceipt(tx.Txid) + if err != nil && err != qtum.EmptyResponseErr { + return nil, err + } + if receipt != nil { + ethTxResp.BlockNumber = hexutil.EncodeUint64(receipt.BlockNumber) + ethTxResp.TransactionIndex = hexutil.EncodeUint64(receipt.TransactionIndex) + ethTxResp.From = utils.AddHexPrefix(receipt.From) + ethTxResp.To = utils.AddHexPrefix(receipt.ContractAddress) + } + } + + return ðTxResp, nil +} + +func (p *ProxyETHGetTransactionByHash) ToRequest(ethreq *eth.GetTransactionByHashRequest) *qtum.GetTransactionRequest { + return &qtum.GetTransactionRequest{ + Txid: utils.RemoveHexPrefix(string(*ethreq)), + } +} diff --git a/pkg/transformer/eth_getTransactionReceipt.go b/pkg/transformer/eth_getTransactionReceipt.go new file mode 100644 index 00000000..89c810bc --- /dev/null +++ b/pkg/transformer/eth_getTransactionReceipt.go @@ -0,0 +1,71 @@ +package transformer + +import ( + "github.com/dcb9/janus/pkg/eth" + "github.com/dcb9/janus/pkg/qtum" + "github.com/dcb9/janus/pkg/utils" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// ProxyETHGetTransactionReceipt implements ETHProxy +type ProxyETHGetTransactionReceipt struct { + *qtum.Qtum +} + +func (p *ProxyETHGetTransactionReceipt) Method() string { + return "eth_getTransactionReceipt" +} + +func (p *ProxyETHGetTransactionReceipt) Request(rawreq *eth.JSONRPCRequest) (interface{}, error) { + var req *eth.GetTransactionReceiptRequest + if err := unmarshalRequest(rawreq.Params, &req); err != nil { + return nil, err + } + + qtumreq, err := p.ToRequest(req) + if err != nil { + return nil, err + } + + return p.request(qtumreq) +} + +func (p *ProxyETHGetTransactionReceipt) request(req *qtum.GetTransactionReceiptRequest) (*eth.GetTransactionReceiptResponse, error) { + var receipt qtum.GetTransactionReceiptResponse + if err := p.Qtum.Request(qtum.MethodGetTransactionReceipt, req, &receipt); err != nil { + if err == qtum.EmptyResponseErr { + return nil, nil + } + return nil, err + } + + status := "0x0" + if receipt.Excepted == "None" { + status = "0x1" + } + + r := qtum.TransactionReceiptStruct(receipt) + logs := getEthLogs(&r) + + ethTxReceipt := eth.GetTransactionReceiptResponse{ + TransactionHash: utils.AddHexPrefix(receipt.TransactionHash), + TransactionIndex: hexutil.EncodeUint64(receipt.TransactionIndex), + BlockHash: utils.AddHexPrefix(receipt.BlockHash), + BlockNumber: hexutil.EncodeUint64(receipt.BlockNumber), + ContractAddress: utils.AddHexPrefix(receipt.ContractAddress), + CumulativeGasUsed: hexutil.EncodeUint64(receipt.CumulativeGasUsed), + GasUsed: hexutil.EncodeUint64(receipt.GasUsed), + Logs: logs, + Status: status, + + // see Known issues + LogsBloom: "", + } + + return ðTxReceipt, nil +} + +func (p *ProxyETHGetTransactionReceipt) ToRequest(ethreq *eth.GetTransactionReceiptRequest) (*qtum.GetTransactionReceiptRequest, error) { + qtumreq := qtum.GetTransactionReceiptRequest(utils.RemoveHexPrefix(string(*ethreq))) + return &qtumreq, nil +} diff --git a/pkg/transformer/eth_net_version.go b/pkg/transformer/eth_net_version.go new file mode 100644 index 00000000..82427cfa --- /dev/null +++ b/pkg/transformer/eth_net_version.go @@ -0,0 +1,29 @@ +package transformer + +import ( + "github.com/dcb9/janus/pkg/eth" + "github.com/dcb9/janus/pkg/qtum" +) + +// ProxyETHNetVersion implements ETHProxy +type ProxyETHNetVersion struct { + *qtum.Qtum +} + +func (p *ProxyETHNetVersion) Method() string { + return "net_version" +} + +func (p *ProxyETHNetVersion) Request(_ *eth.JSONRPCRequest) (interface{}, error) { + return p.request() +} + +func (p *ProxyETHNetVersion) request() (*eth.NetVersionResponse, error) { + var qtumresp *qtum.GetBlockChainInfoResponse + if err := p.Qtum.Request(qtum.MethodGetBlockChainInfo, nil, &qtumresp); err != nil { + return nil, err + } + + resp := eth.NetVersionResponse(qtumresp.Chain) + return &resp, nil +} diff --git a/pkg/transformer/eth_personal_unlockAccount.go b/pkg/transformer/eth_personal_unlockAccount.go new file mode 100644 index 00000000..8ed37a3a --- /dev/null +++ b/pkg/transformer/eth_personal_unlockAccount.go @@ -0,0 +1,16 @@ +package transformer + +import ( + "github.com/dcb9/janus/pkg/eth" +) + +// ProxyETHPersonalUnlockAccount implements ETHProxy +type ProxyETHPersonalUnlockAccount struct{} + +func (p *ProxyETHPersonalUnlockAccount) Method() string { + return "personal_unlockAccount" +} + +func (p *ProxyETHPersonalUnlockAccount) Request(req *eth.JSONRPCRequest) (interface{}, error) { + return eth.PersonalUnlockAccountResponse(true), nil +} diff --git a/pkg/transformer/eth_sendTransaction.go b/pkg/transformer/eth_sendTransaction.go new file mode 100644 index 00000000..9aa3b01f --- /dev/null +++ b/pkg/transformer/eth_sendTransaction.go @@ -0,0 +1,135 @@ +package transformer + +import ( + "github.com/dcb9/janus/pkg/eth" + "github.com/dcb9/janus/pkg/qtum" + "github.com/dcb9/janus/pkg/utils" + "github.com/pkg/errors" +) + +// ProxyETHSendTransaction implements ETHProxy +type ProxyETHSendTransaction struct { + *qtum.Qtum +} + +func (p *ProxyETHSendTransaction) Method() string { + return "eth_sendTransaction" +} + +func (p *ProxyETHSendTransaction) Request(rawreq *eth.JSONRPCRequest) (interface{}, error) { + var req eth.SendTransactionRequest + if err := unmarshalRequest(rawreq.Params, &req); err != nil { + return nil, err + } + + if req.IsCreateContract() { + return p.requestCreateContract(&req) + } else if req.IsSendEther() { + return p.requestSendToAddress(&req) + } else if req.IsCallContract() { + return p.requestSendToContract(&req) + } + + return nil, errors.New("Unknown operation") +} + +func (p *ProxyETHSendTransaction) requestSendToContract(ethtx *eth.SendTransactionRequest) (*eth.SendTransactionResponse, error) { + gasLimit, gasPrice, err := EthGasToQtum(ethtx) + if err != nil { + return nil, err + } + + amount := 0.0 + if ethtx.Value != "" { + var err error + amount, err = EthValueToQtumAmount(ethtx.Value) + if err != nil { + return nil, errors.Wrap(err, "EthValueToQtumAmount:") + } + } + + qtumreq := qtum.SendToContractRequest{ + ContractAddress: utils.RemoveHexPrefix(ethtx.To), + Datahex: utils.RemoveHexPrefix(ethtx.Data), + Amount: amount, + GasLimit: gasLimit, + GasPrice: gasPrice, + } + + if from := ethtx.From; from != "" && utils.IsEthHexAddress(from) { + from, err = p.FromHexAddress(from) + if err != nil { + return nil, err + } + qtumreq.SenderAddress = from + } + + var resp *qtum.SendToContractResponse + if err := p.Qtum.Request(qtum.MethodSendToContract, &qtumreq, &resp); err != nil { + return nil, err + } + + ethresp := eth.SendTransactionResponse(resp.Txid) + return ðresp, nil +} + +func (p *ProxyETHSendTransaction) requestSendToAddress(req *eth.SendTransactionRequest) (*eth.SendTransactionResponse, error) { + getQtumWalletAddress := func(addr string) (string, error) { + if utils.IsEthHexAddress(addr) { + return p.FromHexAddress(utils.RemoveHexPrefix(addr)) + } + return addr, nil + } + + from, err := getQtumWalletAddress(req.From) + if err != nil { + return nil, err + } + + to, err := getQtumWalletAddress(req.To) + if err != nil { + return nil, err + } + + amount, err := EthValueToQtumAmount(req.Value) + if err != nil { + return nil, err + } + + qtumreq := qtum.SendToAddressRequest{ + Address: from, + Amount: amount, + SenderAddress: to, + } + + var qtumresp qtum.SendToAddressResponse + if err := p.Qtum.Request(qtum.MethodSendToAddress, &qtumreq, &qtumresp); err != nil { + return nil, err + } + + ethresp := eth.SendTransactionResponse(utils.AddHexPrefix(string(qtumresp))) + + return ðresp, nil +} + +func (p *ProxyETHSendTransaction) requestCreateContract(req *eth.SendTransactionRequest) (*eth.SendTransactionResponse, error) { + gasLimit, gasPrice, err := EthGasToQtum(req) + if err != nil { + return nil, err + } + + qtumreq := &qtum.CreateContractRequest{ + ByteCode: utils.RemoveHexPrefix(req.Data), + GasLimit: gasLimit, + GasPrice: gasPrice, + } + + var resp *qtum.CreateContractResponse + if err := p.Qtum.Request(qtum.MethodCreateContract, qtumreq, &resp); err != nil { + return nil, err + } + + ethresp := eth.SendTransactionResponse(utils.AddHexPrefix(string(resp.Txid))) + + return ðresp, nil +} diff --git a/pkg/transformer/getlogs.go b/pkg/transformer/getlogs.go deleted file mode 100644 index d9328820..00000000 --- a/pkg/transformer/getlogs.go +++ /dev/null @@ -1,162 +0,0 @@ -package transformer - -import ( - "encoding/json" - "errors" - "math/big" - - "github.com/dcb9/go-ethereum/common/hexutil" - "github.com/dcb9/janus/pkg/eth" - "github.com/dcb9/janus/pkg/qtum" - "github.com/dcb9/janus/pkg/rpc" -) - -func (m *Manager) GetLogs(req *rpc.JSONRPCRequest) (ResponseTransformerFunc, error) { - var params []json.RawMessage - if err := unmarshalRequest(req.Params, ¶ms); err != nil { - return nil, err - } - - var filter eth.GetLogsFilter - if len(params) > 0 { - if err := unmarshalRequest(params[0], &filter); err != nil { - return nil, err - } - } - - if len(filter.Topics) != 0 { - return nil, errors.New("topics is not supported yet") - } - - from, err := m.getQtumBlockNumber(filter.FromBlock, 0) - if err != nil { - return nil, err - } - - to, err := m.getQtumBlockNumber(filter.ToBlock, -1) - if err != nil { - return nil, err - } - - var addresses []string - if filter.Address != nil { - if filter.Address[0] == '"' { - var addr string - if err = json.Unmarshal(filter.Address, &addr); err != nil { - return nil, err - } - addresses = append(addresses, addr) - } else { - if err = json.Unmarshal(filter.Address, &addresses); err != nil { - return nil, err - } - } - for i, _ := range addresses { - addresses[i] = RemoveHexPrefix(addresses[i]) - } - } - - newParams, err := json.Marshal([]interface{}{ - from, - to, - map[string][]string{ - "addresses": addresses, - }, - }) - - req.Params = newParams - req.Method = qtum.MethodSearchlogs - - return m.GetLogsResp, nil -} - -func (m *Manager) GetLogsResp(result json.RawMessage) (interface{}, error) { - var receipts []*qtum.TransactionReceipt - if err := json.Unmarshal(result, &receipts); err != nil { - return nil, err - } - - logs := make([]eth.Log, 0) - for _, receipt := range receipts { - logs = append(logs, getEthLogs(receipt)...) - } - - return logs, nil -} - -func (m *Manager) getQtumBlockNumber(ethBlock json.RawMessage, defaultVal int64) (*big.Int, error) { - if ethBlock == nil { - return big.NewInt(defaultVal), nil - } - - if isString(ethBlock) { - var ethBlockStr string - if err := json.Unmarshal(ethBlock, ðBlockStr); err != nil { - return nil, err - } - - switch ethBlockStr { - case "latest": - return big.NewInt(-1), nil - case "pending", "earliest": - return nil, errors.New(`tags, "pending" and "earliest", are unsupported`) - default: - return hexutil.DecodeBig(ethBlockStr) - } - } - - var b int64 - if err := json.Unmarshal(ethBlock, &b); err != nil { - return nil, err - } - - return big.NewInt(b), nil -} - -func isString(v json.RawMessage) bool { - return v[0] == '"' -} - -func getEthLogs(receipt *qtum.TransactionReceipt) []eth.Log { - logs := make([]eth.Log, 0, len(receipt.Log)) - for index, log := range receipt.Log { - topics := make([]string, 0, len(log.Topics)) - for _, topic := range log.Topics { - topics = append(topics, AddHexPrefix(topic)) - } - logs = append(logs, eth.Log{ - TransactionHash: AddHexPrefix(receipt.TransactionHash), - TransactionIndex: hexutil.EncodeUint64(receipt.TransactionIndex), - BlockHash: AddHexPrefix(receipt.BlockHash), - BlockNumber: hexutil.EncodeUint64(receipt.BlockNumber), - Data: AddHexPrefix(log.Data), - Address: AddHexPrefix(log.Address), - Topics: topics, - LogIndex: hexutil.EncodeUint64(uint64(index)), - }) - } - return logs -} - -// Eth -// eth_getLogs -//fromBlock: QUANTITY|TAG - (optional, default: "latest") Integer block number, or "latest" for the last mined block or "pending", "earliest" for not yet mined transactions. -//toBlock: QUANTITY|TAG - (optional, default: "latest") Integer block number, or "latest" for the last mined block or "pending", "earliest" for not yet mined transactions. -//address: DATA|Array, 20 Bytes - (optional) Contract address or a list of addresses from which logs should originate. -//topics: Array of DATA, - (optional) Array of 32 Bytes DATA topics. Topics are order-dependent. Each topic can also be an array of DATA with "or" options. -//blockhash: DATA, 32 Bytes - (optional, future) With the addition of EIP-234, blockHash will be a new filter option which restricts the logs returned to the single block with the 32-byte hash blockHash. Using blockHash is equivalent to fromBlock = toBlock = the block number with hash blockHash. If blockHash is present in in the filter criteria, then neither fromBlock nor toBlock are allowed. - -// Qtum -//+ qcli help searchlogs -//searchlogs (address) (topics) -//requires -logevents to be enabled -//Argument: -//1. "fromBlock" (numeric, required) The number of the earliest block (latest may be given to mean the most recent block). -//2. "toBlock" (string, required) The number of the latest block (-1 may be given to mean the most recent block). -//3. "address" (string, optional) An address or a list of addresses to only get logs from particular account(s). -//4. "topics" (string, optional) An array of values from which at least one must appear in the log entries. The order is important, if you want to leave topics out use null, e.g. ["null", "0x00..."]. -//5. "minconf" (uint, optional, default=0) Minimal number of confirmations before a log is returned -// -//Examples: -//> qtum-cli searchlogs 0 100 '{"addresses": ["12ae42729af478ca92c8c66773a3e32115717be4"]}' '{"topics": ["null","b436c2bf863ccd7b8f63171201efd4792066b4ce8e543dde9c3e9e9ab98e216c"]}' -//> curl --user myusername --data-binary '{"jsonrpc": "1.0", "id":"curltest", "method": "searchlogs", "params": [0 100 {"addresses": ["12ae42729af478ca92c8c66773a3e32115717be4"]} {"topics": ["null","b436c2bf863ccd7b8f63171201efd4792066b4ce8e543dde9c3e9e9ab98e216c"]}] }' -H 'content-type: text/plain;' http://127.0.0.1:3889/ diff --git a/pkg/transformer/gettransaction.go b/pkg/transformer/gettransaction.go deleted file mode 100644 index 81bc83b1..00000000 --- a/pkg/transformer/gettransaction.go +++ /dev/null @@ -1,163 +0,0 @@ -package transformer - -import ( - "encoding/json" - - "math/big" - - "github.com/dcb9/janus/pkg/eth" - "github.com/dcb9/janus/pkg/qtum" - "github.com/dcb9/janus/pkg/rpc" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/pkg/errors" -) - -func (m *Manager) GetTransactionByHash(req *rpc.JSONRPCRequest) (ResponseTransformerFunc, error) { - var params []string - if err := unmarshalRequest(req.Params, ¶ms); err != nil { - return nil, err - } - if len(params) == 0 { - return nil, errors.New("params must be set") - } - - txid := RemoveHexPrefix(params[0]) - newParams, err := json.Marshal([]interface{}{ - txid, - }) - if err != nil { - return nil, err - } - - req.Params = newParams - req.Method = qtum.MethodGettransaction - - return m.GettransactionResp, nil -} - -func (m *Manager) GettransactionResp(result json.RawMessage) (interface{}, error) { - var tx *qtum.Transaction - err := json.Unmarshal(result, &tx) - if err != nil { - return nil, err - } - - ethVal, err := QtumAmountToEthValue(tx.Amount) - if err != nil { - return nil, err - } - - decodedRawTx, err := m.qtumClient.DecodeRawTransaction(tx.Hex) - if err != nil { - return nil, errors.Wrap(err, "Manager#GettransactionResp") - } - var gas, gasPrice, input string - type asmWithGasGasPriceEncodedABI interface { - GetEncodedABI() string - GetGasPrice() (*big.Int, error) - GetGasLimit() (*big.Int, error) - } - - var asm asmWithGasGasPriceEncodedABI - for _, out := range decodedRawTx.Vout { - switch out.ScriptPubKey.Type { - case "call": - if asm, err = qtum.ParseCallASM(out.ScriptPubKey.Asm); err != nil { - return nil, err - } - case "create": - if asm, err = qtum.ParseCreateASM(out.ScriptPubKey.Asm); err != nil { - return nil, err - } - default: - continue - } - break - } - - if asm != nil { - input = AddHexPrefix(asm.GetEncodedABI()) - gasLimitBigInt, err := asm.GetGasLimit() - if err != nil { - return nil, err - } - gasPriceBigInt, err := asm.GetGasPrice() - if err != nil { - return nil, err - } - gas = hexutil.EncodeBig(gasLimitBigInt) - gasPrice = hexutil.EncodeBig(gasPriceBigInt) - } - - ethTxResp := eth.TransactionResponse{ - Hash: AddHexPrefix(tx.Txid), - BlockHash: AddHexPrefix(tx.Blockhash), - Nonce: "", - Value: ethVal, - Input: input, - Gas: gas, - GasPrice: gasPrice, - } - - if asm != nil { - receipt, err := m.qtumClient.GetTransactionReceipt(tx.Txid) - if err != nil { - return nil, err - } - if receipt != nil { - ethTxResp.BlockNumber = hexutil.EncodeUint64(receipt.BlockNumber) - ethTxResp.TransactionIndex = hexutil.EncodeUint64(receipt.TransactionIndex) - ethTxResp.From = AddHexPrefix(receipt.From) - ethTxResp.To = AddHexPrefix(receipt.ContractAddress) - } - } - - return ðTxResp, nil -} - -//Qtum RPC -// gettransaction "txid" ( include_watchonly ) (waitconf) -// -// Get detailed information about in-wallet transaction -// -// Arguments: -// 1. "txid" (string, required) The transaction id -// 2. "include_watchonly" (bool, optional, default=false) Whether to include watch-only addresses in balance calculation and details[] -// 3. "waitconf" (int, optional, default=0) Wait for enough confirmations before returning -// -// Result: -// { -// "amount" : x.xxx, (numeric) The transaction amount in QTUM -// "fee": x.xxx, (numeric) The amount of the fee in QTUM. This is negative and only available for the -// 'send' category of transactions. -// "confirmations" : n, (numeric) The number of confirmations -// "blockhash" : "hash", (string) The block hash -// "blockindex" : xx, (numeric) The index of the transaction in the block that includes it -// "blocktime" : ttt, (numeric) The time in seconds since epoch (1 Jan 1970 GMT) -// "txid" : "transactionid", (string) The transaction id. -// "time" : ttt, (numeric) The transaction time in seconds since epoch (1 Jan 1970 GMT) -// "timereceived" : ttt, (numeric) The time received in seconds since epoch (1 Jan 1970 GMT) -// "bip125-replaceable": "yes|no|unknown", (string) Whether this transaction could be replaced due to BIP125 (replace-by-fee); -// may be unknown for unconfirmed transactions not in the mempool -// "details" : [ -// { -// "account" : "accountname", (string) DEPRECATED. The account name involved in the transaction, can be "" for the default account. -// "address" : "address", (string) The qtum address involved in the transaction -// "category" : "send|receive", (string) The category, either 'send' or 'receive' -// "amount" : x.xxx, (numeric) The amount in QTUM -// "label" : "label", (string) A comment for the address/transaction, if any -// "vout" : n, (numeric) the vout value -// "fee": x.xxx, (numeric) The amount of the fee in QTUM. This is negative and only available for the -// 'send' category of transactions. -// "abandoned": xxx (bool) 'true' if the transaction has been abandoned (inputs are respendable). Only available for the -// 'send' category of transactions. -// } -// ,... -// ], -// "hex" : "data" (string) Raw data for transaction -// } -// -// Examples: -// > qtum-cli gettransaction "1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d" -// > qtum-cli gettransaction "1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d" true -// > curl --user myusername --data-binary '{"jsonrpc": "1.0", "id":"curltest", "method": "gettransaction", "params": ["1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"] }' -H 'content-type: text/plain;' http://127.0.0.1:3889/ diff --git a/pkg/transformer/gettransactionreceipt.go b/pkg/transformer/gettransactionreceipt.go deleted file mode 100644 index 18a904c8..00000000 --- a/pkg/transformer/gettransactionreceipt.go +++ /dev/null @@ -1,87 +0,0 @@ -package transformer - -import ( - "encoding/json" - "errors" - - "github.com/bitly/go-simplejson" - "github.com/dcb9/janus/pkg/eth" - "github.com/dcb9/janus/pkg/qtum" - "github.com/dcb9/janus/pkg/rpc" - "github.com/ethereum/go-ethereum/common/hexutil" -) - -func (m *Manager) GetTransactionReceipt(req *rpc.JSONRPCRequest) (ResponseTransformerFunc, error) { - var params []string - if err := unmarshalRequest(req.Params, ¶ms); err != nil { - return nil, &rpc.JSONRPCError{ - Code: rpc.ErrInvalid, - Message: "invalid input", - } - } - if len(params) == 0 { - return nil, errors.New("params must be set") - } - - newParams, err := json.Marshal([]string{ - RemoveHexPrefix(params[0]), - }) - if err != nil { - return nil, err - } - - req.Params = newParams - req.Method = qtum.MethodGettransactionreceipt - - //Qtum RPC - //gettransactionreceipt "hash" - // requires -logevents to be enabled - // Argument: - // 1. "hash" (string, required) The transaction hash - - return m.GettransactionreceiptResp, nil -} - -func (m *Manager) GettransactionreceiptResp(result json.RawMessage) (interface{}, error) { - if string(result) == "[]" { - return nil, nil - } - - sj, err := simplejson.NewJson(result) - if err != nil { - return nil, err - } - - receiptBytes, err := sj.GetIndex(0).Encode() - if err != nil { - return nil, err - } - var receipt *qtum.TransactionReceipt - if err = json.Unmarshal(receiptBytes, &receipt); err != nil { - return nil, err - } - - status := "0x0" - if receipt.Excepted == "None" { - status = "0x1" - } - - logs := getEthLogs(receipt) - - ethTxReceipt := eth.TransactionReceipt{ - TransactionHash: AddHexPrefix(receipt.TransactionHash), - TransactionIndex: hexutil.EncodeUint64(receipt.TransactionIndex), - BlockHash: AddHexPrefix(receipt.BlockHash), - BlockNumber: hexutil.EncodeUint64(receipt.BlockNumber), - ContractAddress: AddHexPrefix(receipt.ContractAddress), - CumulativeGasUsed: hexutil.EncodeUint64(receipt.CumulativeGasUsed), - GasUsed: hexutil.EncodeUint64(receipt.GasUsed), - Logs: logs, - Status: status, - - // see Known issues - LogsBloom: "", - } - - return ðTxReceipt, nil -} diff --git a/pkg/transformer/manager.go b/pkg/transformer/manager.go deleted file mode 100644 index e51091c9..00000000 --- a/pkg/transformer/manager.go +++ /dev/null @@ -1,145 +0,0 @@ -package transformer - -import ( - "encoding/json" - "math/big" - - "github.com/bitly/go-simplejson" - "github.com/dcb9/janus/pkg/eth" - "github.com/dcb9/janus/pkg/qtum" - "github.com/dcb9/janus/pkg/rpc" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/go-kit/kit/log" - "github.com/pkg/errors" -) - -type Manager struct { - qtumClient *qtum.Client - debug bool - logger log.Logger - requestTransformers map[string]RequestTransformerFunc -} - -func NewManager(qtumClient *qtum.Client, opts ...func(*Manager) error) (*Manager, error) { - if qtumClient == nil { - return nil, errors.New("qtumClient cannot be nil") - } - - m := &Manager{ - qtumClient: qtumClient, - logger: log.NewNopLogger(), - } - m.requestTransformers = map[string]RequestTransformerFunc{ - "eth_sendTransaction": m.SendTransaction, - "eth_call": m.Call, - "eth_getTransactionByHash": m.GetTransactionByHash, - "eth_getTransactionReceipt": m.GetTransactionReceipt, - "eth_blockNumber": m.BlockNumber, - "net_version": m.NetVersion, - "eth_getLogs": m.GetLogs, - } - - for _, opt := range opts { - if err := opt(m); err != nil { - return nil, err - } - } - - return m, nil -} - -func (m *Manager) TransformRequest(rpcReq *rpc.JSONRPCRequest) (ResponseTransformerFunc, error) { - if trafo, ok := m.requestTransformers[rpcReq.Method]; ok { - return trafo(rpcReq) - } - - return nil, nil -} - -func (m *Manager) SendTransaction(req *rpc.JSONRPCRequest) (ResponseTransformerFunc, error) { - var txs []*eth.TransactionReq - if err := unmarshalRequest(req.Params, &txs); err != nil { - return nil, err - } - if len(txs) == 0 { - return nil, errors.New("params must be set") - } - - var err error - t := txs[0] - if t.IsCreateContract() { - err = m.createcontract(req, t) - } else if t.IsSendEther() { - return m.sendtoaddress(req, t) - } else if t.IsCallContract() { - err = m.sendtocontract(req, t) - } else { - err = &rpc.JSONRPCError{ - Code: rpc.ErrUnknownOperation, - Message: "unknown operation", - } - } - if err != nil { - return nil, err - } - - return m.sendTransactionResp, nil -} -func (m *Manager) sendTransactionResp(result json.RawMessage) (interface{}, error) { - sj, err := simplejson.NewJson(result) - if err != nil { - return nil, err - } - txid, err := sj.Get("txid").String() - if err != nil { - return nil, err - } - - return AddHexPrefix(txid), nil -} - -func (m *Manager) BlockNumber(req *rpc.JSONRPCRequest) (ResponseTransformerFunc, error) { - req.Method = qtum.MethodGetblockcount - - return func(result json.RawMessage) (newResult interface{}, err error) { - var n *big.Int - if err := json.Unmarshal(result, &n); err != nil { - return nil, err - } - return hexutil.EncodeBig(n), nil - }, nil -} - -func (m *Manager) NetVersion(req *rpc.JSONRPCRequest) (ResponseTransformerFunc, error) { - req.Method = qtum.MethodGetblockchaininfo - - return func(result json.RawMessage) (newResult interface{}, err error) { - var i *qtum.BlockChainInfo - if err := json.Unmarshal(result, &i); err != nil { - return nil, err - } - - return i.Chain, nil - }, nil -} - -func SetDebug(debug bool) func(*Manager) error { - return func(m *Manager) error { - m.debug = debug - return nil - } -} - -func SetLogger(l log.Logger) func(*Manager) error { - return func(m *Manager) error { - m.logger = log.WithPrefix(l, "component", "transformer") - return nil - } -} - -func (m *Manager) getQtumWalletAddress(addr string) (string, error) { - if IsEthHexAddress(addr) { - return m.qtumClient.FromHexAddress(RemoveHexPrefix(addr)) - } - return addr, nil -} diff --git a/pkg/transformer/manager_test.go b/pkg/transformer/manager_test.go deleted file mode 100644 index e8c189e6..00000000 --- a/pkg/transformer/manager_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package transformer - -import "testing" - -func TestRemoveHexPrefix(t *testing.T) { - cases := []map[string]string{ - { - "in": "0x8d124864e8840a114a8772c1daf82b61eb4dca01", - "want": "8d124864e8840a114a8772c1daf82b61eb4dca01", - }, - { - "in": "8d124864e8840a114a8772c1daf82b61eb4dca01", - "want": "8d124864e8840a114a8772c1daf82b61eb4dca01", - }, - { - "in": "", - "want": "", - }, - } - - for _, c := range cases { - in, want := c["in"], c["want"] - if got := RemoveHexPrefix(in); got != want { - t.Errorf("err: in: %s, want: %s, got: %s", in, want, got) - } - } -} diff --git a/pkg/transformer/sendtoaddress.go b/pkg/transformer/sendtoaddress.go deleted file mode 100644 index 812c6500..00000000 --- a/pkg/transformer/sendtoaddress.go +++ /dev/null @@ -1,90 +0,0 @@ -package transformer - -import ( - "encoding/json" - - "github.com/dcb9/janus/pkg/eth" - "github.com/dcb9/janus/pkg/qtum" - "github.com/dcb9/janus/pkg/rpc" -) - -func (m *Manager) sendtoaddress(req *rpc.JSONRPCRequest, tx *eth.TransactionReq) (ResponseTransformerFunc, error) { - req.Method = qtum.MethodSendtoaddress - - from, err := m.getQtumWalletAddress(tx.From) - if err != nil { - return nil, err - } - to, err := m.getQtumWalletAddress(tx.To) - if err != nil { - return nil, err - } - amount, err := EthValueToQtumAmount(tx.Value) - if err != nil { - return nil, err - } - - params := []interface{}{ - to, - amount, - "", // comment - "", // comment_to - false, - nil, - nil, - nil, - from, - true, - } - - req.Params, err = json.Marshal(params) - if err != nil { - return nil, err - } - - return m.SendtoaddressResp, nil -} -func (m *Manager) SendtoaddressResp(result json.RawMessage) (interface{}, error) { - var txid string - err := json.Unmarshal(result, &txid) - if err != nil { - return nil, err - } - - return AddHexPrefix(txid), nil -} - -// $ qcli help sendtoaddress -// sendtoaddress "address" amount ( "comment" "comment_to" subtractfeefromamount replaceable conf_target "estimate_mode") -// -// Send an amount to a given address. -// -// Arguments: -// 1. "address" (string, required) The qtum address to send to. -// 2. "amount" (numeric or string, required) The amount in QTUM to send. eg 0.1 -// 3. "comment" (string, optional) A comment used to store what the transaction is for. -// This is not part of the transaction, just kept in your wallet. -// 4. "comment_to" (string, optional) A comment to store the name of the person or organization -// to which you're sending the transaction. This is not part of the -// transaction, just kept in your wallet. -// 5. subtractfeefromamount (boolean, optional, default=false) The fee will be deducted from the amount being sent. -// The recipient will receive less qtums than you enter in the amount field. -// 6. replaceable (boolean, optional) Allow this transaction to be replaced by a transaction with higher fees via BIP 125 -// 7. conf_target (numeric, optional) Confirmation target (in blocks) -// 8. "estimate_mode" (string, optional, default=UNSET) The fee estimate mode, must be one of: -// "UNSET" -// "ECONOMICAL" -// "CONSERVATIVE" -// 9. "senderaddress" (string, optional) The quantum address that will be used to send money from. -// 10."changeToSender" (bool, optional, default=false) Return the change to the sender. -// -// Result: -// "txid" (string) The transaction id. -// -// Examples: -// > qtum-cli sendtoaddress "QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd" 0.1 -// > qtum-cli sendtoaddress "QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd" 0.1 "donation" "seans outpost" -// > qtum-cli sendtoaddress "QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd" 0.1 "" "" true -// > qtum-cli sendtoaddress "QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd", 0.1, "donation", "seans outpost", false, null, null, "", "QX1GkJdye9WoUnrE2v6ZQhQ72EUVDtGXQX", true -// > curl --user myusername --data-binary '{"jsonrpc": "1.0", "id":"curltest", "method": "sendtoaddress", "params": ["QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd", 0.1, "donation", "seans outpost"] }' -H 'content-type: text/plain;' http://127.0.0.1:3889/ -// > curl --user myusername --data-binary '{"jsonrpc": "1.0", "id":"curltest", "method": "sendtoaddress", "params": ["QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd", 0.1, "donation", "seans outpost", false, null, null, "", "QX1GkJdye9WoUnrE2v6ZQhQ72EUVDtGXQX", true] }' -H 'content-type: text/plain;' http://127.0.0.1:3889/ diff --git a/pkg/transformer/sendtocontract.go b/pkg/transformer/sendtocontract.go deleted file mode 100644 index d6275acd..00000000 --- a/pkg/transformer/sendtocontract.go +++ /dev/null @@ -1,91 +0,0 @@ -package transformer - -import ( - "encoding/json" - - "github.com/dcb9/janus/pkg/eth" - "github.com/dcb9/janus/pkg/qtum" - "github.com/dcb9/janus/pkg/rpc" - "github.com/pkg/errors" -) - -func (m *Manager) sendtocontract(req *rpc.JSONRPCRequest, tx *eth.TransactionReq) error { - gasLimit, gasPrice, err := EthGasToQtum(tx) - if err != nil { - return err - } - - amount := 0.0 - if tx.Value != "" { - var err error - amount, err = EthValueToQtumAmount(tx.Value) - if err != nil { - return errors.Wrap(err, "EthValueToQtumAmount:") - } - } - - params := []interface{}{ - RemoveHexPrefix(tx.To), - RemoveHexPrefix(tx.Data), - amount, - gasLimit, - gasPrice, - } - - if from := tx.From; from != "" { - if IsEthHexAddress(from) { - from, err = m.qtumClient.FromHexAddress(RemoveHexPrefix(from)) - if err != nil { - return err - } - } - params = append(params, from) - } - - newParams, err := json.Marshal(params) - if err != nil { - return err - } - - req.Params = newParams - req.Method = qtum.MethodSendtocontract - - return nil -} - -// Eth RPC -// params: [{ -// "from": "0xb60e8dd61c5d32be8058bb8eb970870f07233155", -// "to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567", -// "gas": "0x76c0", // 30400 -// "gasPrice": "0x9184e72a000", // 10000000000000 -// "value": "0x9184e72a", // 2441406250 -// "data": "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675" -// }] - -//Qtum RPC -// sendtocontract "contractaddress" "data" (amount gaslimit gasprice senderaddress broadcast) -// Send funds and data to a contract. -// -//Arguments: -// 1. "contractaddress" (string, required) The contract address that will receive the funds and data. -// 2. "datahex" (string, required) data to send. -// 3. "amount" (numeric or string, optional) The amount in QTUM to send. eg 0.1, default: 0 -// 4. gasLimit (numeric or string, optional) gasLimit, default: 250000, max: 40000000 -// 5. gasPrice (numeric or string, optional) gasPrice Qtum price per gas unit, default: 0.0000004, min:0.0000004 -// 6. "senderaddress" (string, optional) The quantum address that will be used as sender. -// 7. "broadcast" (bool, optional, default=true) Whether to broadcast the transaction or not. -// 8. "changeToSender" (bool, optional, default=true) Return the change to the sender. -// -//Result: -// [ -// { -// "txid" : (string) The transaction id. -// "sender" : (string) QTUM address of the sender. -// "hash160" : (string) ripemd-160 hash of the sender. -// } -// ] -// -//Examples: -// > qtum-cli sendtocontract "c6ca2697719d00446d4ea51f6fac8fd1e9310214" "54f6127f" -// > qtum-cli sendtocontract "c6ca2697719d00446d4ea51f6fac8fd1e9310214" "54f6127f" 12.0015 6000000 0.0000004 "QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd" diff --git a/pkg/transformer/transformer.go b/pkg/transformer/transformer.go new file mode 100644 index 00000000..7bef5b9d --- /dev/null +++ b/pkg/transformer/transformer.go @@ -0,0 +1,101 @@ +package transformer + +import ( + "github.com/dcb9/janus/pkg/eth" + "github.com/dcb9/janus/pkg/qtum" + "github.com/go-kit/kit/log" + "github.com/pkg/errors" +) + +type Transformer struct { + qtumClient *qtum.Qtum + debugMode bool + logger log.Logger + transformers map[string]ETHProxy +} + +func New(qtumClient *qtum.Qtum, proxies []ETHProxy, opts ...Option) (*Transformer, error) { + if qtumClient == nil { + return nil, errors.New("qtumClient cannot be nil") + } + + t := &Transformer{ + qtumClient: qtumClient, + logger: log.NewNopLogger(), + } + + var err error + for _, p := range proxies { + if err = t.Register(p); err != nil { + return nil, err + } + } + + for _, opt := range opts { + if err := opt(t); err != nil { + return nil, err + } + } + + return t, nil +} + +func (t *Transformer) Register(p ETHProxy) error { + if t.transformers == nil { + t.transformers = make(map[string]ETHProxy) + } + + m := p.Method() + if _, ok := t.transformers[m]; ok { + return errors.Errorf("method %s is exist", m) + } + + t.transformers[m] = p + + return nil +} + +func (t *Transformer) Transform(rpcReq *eth.JSONRPCRequest) (interface{}, error) { + p, err := t.getProxy(rpcReq) + if err != nil { + return nil, err + } + + return p.Request(rpcReq) +} + +func (t *Transformer) getProxy(rpcReq *eth.JSONRPCRequest) (ETHProxy, error) { + m := rpcReq.Method + p, ok := t.transformers[m] + if !ok { + return nil, errors.Errorf("Unsupported method %s", m) + } + return p, nil +} + +func DefaultProxies(qtumRPCClient *qtum.Qtum) []ETHProxy { + return []ETHProxy{ + &ProxyETHCall{Qtum: qtumRPCClient}, + &ProxyETHPersonalUnlockAccount{}, + &ProxyETHBlockNumber{Qtum: qtumRPCClient}, + &ProxyETHNetVersion{Qtum: qtumRPCClient}, + &ProxyETHGetTransactionByHash{Qtum: qtumRPCClient}, + &ProxyETHGetLogs{Qtum: qtumRPCClient}, + &ProxyETHGetTransactionReceipt{Qtum: qtumRPCClient}, + &ProxyETHSendTransaction{Qtum: qtumRPCClient}, + } +} + +func SetDebug(debug bool) func(*Transformer) error { + return func(t *Transformer) error { + t.debugMode = debug + return nil + } +} + +func SetLogger(l log.Logger) func(*Transformer) error { + return func(t *Transformer) error { + t.logger = log.WithPrefix(l, "component", "transformer") + return nil + } +} diff --git a/pkg/transformer/type.go b/pkg/transformer/type.go index dc51919b..dbca2d53 100644 --- a/pkg/transformer/type.go +++ b/pkg/transformer/type.go @@ -1,17 +1,16 @@ package transformer import ( - "encoding/json" + "errors" - "github.com/dcb9/janus/pkg/rpc" - "github.com/pkg/errors" + "github.com/dcb9/janus/pkg/eth" ) -type ( - RequestTransformerFunc func(*rpc.JSONRPCRequest) (ResponseTransformerFunc, error) - ResponseTransformerFunc func(result json.RawMessage) (newResult interface{}, err error) -) +var UnmarshalRequestErr = errors.New("Input is invalid") -var ( - UnmarshalRequestErr = errors.New("unmarshal request error") -) +type Option func(*Transformer) error + +type ETHProxy interface { + Request(*eth.JSONRPCRequest) (interface{}, error) + Method() string +} diff --git a/pkg/transformer/util.go b/pkg/transformer/util.go index e2e532a2..e3acb15b 100644 --- a/pkg/transformer/util.go +++ b/pkg/transformer/util.go @@ -4,29 +4,28 @@ import ( "encoding/json" "fmt" "math/big" - "strings" - "github.com/ethereum/go-ethereum/common" + "github.com/dcb9/janus/pkg/utils" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" ) type EthGas interface { - GetGas() string - GetGasPrice() string + GasHex() string + GasPriceHex() string } func EthGasToQtum(g EthGas) (gasLimit *big.Int, gasPrice string, err error) { gasLimit = big.NewInt(2500000) - if gas := g.GetGas(); gas != "" { - gasLimit, err = hexutil.DecodeBig(AddHexPrefix(gas)) + if gas := g.GasHex(); gas != "" { + gasLimit, err = utils.DecodeBig(gas) if err != nil { err = errors.Wrap(err, "decode gas") return } } - gasPriceFloat64, err := EthValueToQtumAmount(g.GetGasPrice()) + gasPriceFloat64, err := EthValueToQtumAmount(g.GasPriceHex()) if err != nil { return nil, "0.0", err } @@ -36,7 +35,7 @@ func EthGasToQtum(g EthGas) (gasLimit *big.Int, gasPrice string, err error) { } func EthValueToQtumAmount(val string) (float64, error) { - ethVal, err := hexutil.DecodeBig(AddHexPrefix(val)) + ethVal, err := utils.DecodeBig(val) if err != nil { return 0.0, err } @@ -66,24 +65,6 @@ func QtumAmountToEthValue(amount float64) (string, error) { return hexutil.EncodeBig(result), nil } -func RemoveHexPrefix(hex string) string { - if strings.HasPrefix(hex, "0x") { - return hex[2:] - } - return hex -} - -func IsEthHexAddress(str string) bool { - return strings.HasPrefix(str, "0x") || common.IsHexAddress("0x"+str) -} - -func AddHexPrefix(hex string) string { - if strings.HasPrefix(hex, "0x") { - return hex - } - return "0x" + hex -} - func unmarshalRequest(data []byte, v interface{}) error { if err := json.Unmarshal(data, v); err != nil { return errors.Wrap(UnmarshalRequestErr, err.Error()) diff --git a/pkg/transformer/util_test.go b/pkg/transformer/util_test.go index f87fe24c..e7b2e7c5 100644 --- a/pkg/transformer/util_test.go +++ b/pkg/transformer/util_test.go @@ -1,10 +1,7 @@ package transformer import ( - "math/big" "testing" - - "github.com/dcb9/janus/pkg/eth" ) func TestEthValueToQtumAmount(t *testing.T) { @@ -42,47 +39,3 @@ func TestQtumAmountToEthValue(t *testing.T) { t.Errorf("in: %f, want: %s, got: %s", in, want, got) } } - -func TestEthGasToQtum(t *testing.T) { - cases := []map[string]interface{}{ - { - "in": ð.TransactionReq{ - Gas: "0x64", - GasPrice: "0x1", - }, - "wantGas": big.NewInt(100), - "wantGasPrice": "0.00000001", - }, - { - "in": ð.TransactionReq{ - Gas: "0x1", - GasPrice: "0xff", - }, - "wantGas": big.NewInt(1), - "wantGasPrice": "0.00000255", - }, - { - "in": ð.TransactionReq{ - Gas: "0x1", - GasPrice: "0x64", - }, - "wantGas": big.NewInt(1), - "wantGasPrice": "0.00000100", - }, - } - - for _, c := range cases { - in := c["in"].(*eth.TransactionReq) - wantGas, wantGasPrice := c["wantGas"].(*big.Int), c["wantGasPrice"].(string) - gotGas, gotGasPrice, err := EthGasToQtum(in) - if err != nil { - t.Error(err) - } - if gotGas.Cmp(wantGas) != 0 { - t.Errorf("get Gas error in: %s, want: %d, got: %d", in.Gas, wantGas, gotGas.Int64()) - } - if gotGasPrice != wantGasPrice { - t.Errorf("get GasPrice error in: %s, want: %s, got: %s", in.GasPrice, wantGasPrice, gotGasPrice) - } - } -} diff --git a/pkg/utils/hex.go b/pkg/utils/hex.go new file mode 100644 index 00000000..d6e9d01c --- /dev/null +++ b/pkg/utils/hex.go @@ -0,0 +1,33 @@ +package utils + +import ( + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +func RemoveHexPrefix(hex string) string { + if strings.HasPrefix(hex, "0x") { + return hex[2:] + } + return hex +} + +func IsEthHexAddress(str string) bool { + return strings.HasPrefix(str, "0x") || common.IsHexAddress("0x"+str) +} + +func AddHexPrefix(hex string) string { + if strings.HasPrefix(hex, "0x") { + return hex + } + return "0x" + hex +} + +// DecodeBig decodes a hex string whether input is with 0x prefix or not. +func DecodeBig(input string) (*big.Int, error) { + input = AddHexPrefix(input) + return hexutil.DecodeBig(input) +} diff --git a/playground/.gitignore b/playground/.gitignore index a512ba8b..a908a9ae 100644 --- a/playground/.gitignore +++ b/playground/.gitignore @@ -73,3 +73,4 @@ typings/ .serverless solar*.json /yarn.lock +/.boast.json