blockbook/bchain/coins/nuls/nulsrpc.go

629 lines
15 KiB
Go

package nuls
import (
"bytes"
"encoding/base64"
"encoding/hex"
"encoding/json"
"io"
"io/ioutil"
"math/big"
"net"
"net/http"
"runtime/debug"
"strconv"
"time"
"github.com/golang/glog"
"github.com/juju/errors"
"spacecruft.org/spacecruft/blockbook/bchain"
"spacecruft.org/spacecruft/blockbook/bchain/coins/btc"
)
// NulsRPC is an interface to JSON-RPC bitcoind service
type NulsRPC struct {
*btc.BitcoinRPC
client http.Client
rpcURL string
user string
password string
}
// NewNulsRPC returns new NulsRPC instance
func NewNulsRPC(config json.RawMessage, pushHandler func(bchain.NotificationType)) (bchain.BlockChain, error) {
b, err := btc.NewBitcoinRPC(config, pushHandler)
if err != nil {
return nil, err
}
var c btc.Configuration
err = json.Unmarshal(config, &c)
if err != nil {
return nil, errors.Annotatef(err, "Invalid configuration file")
}
transport := &http.Transport{
Dial: (&net.Dialer{KeepAlive: 600 * time.Second}).Dial,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100, // necessary to not to deplete ports
}
s := &NulsRPC{
BitcoinRPC: b.(*btc.BitcoinRPC),
client: http.Client{Timeout: time.Duration(c.RPCTimeout) * time.Second, Transport: transport},
rpcURL: c.RPCURL,
user: c.RPCUser,
password: c.RPCPass,
}
s.BitcoinRPC.RPCMarshaler = btc.JSONMarshalerV1{}
s.BitcoinRPC.ChainConfig.SupportsEstimateSmartFee = false
return s, nil
}
// Initialize initializes GincoinRPC instance.
func (n *NulsRPC) Initialize() error {
chainName := ""
params := GetChainParams(chainName)
// always create parser
n.BitcoinRPC.Parser = NewNulsParser(params, n.BitcoinRPC.ChainConfig)
// parameters for getInfo request
if params.Net == MainnetMagic {
n.BitcoinRPC.Testnet = false
n.BitcoinRPC.Network = "livenet"
} else {
n.BitcoinRPC.Testnet = true
n.BitcoinRPC.Network = "testnet"
}
glog.Info("rpc: block chain ", params.Name)
return nil
}
type CmdGetNetworkInfo struct {
Success bool `json:"success"`
Data struct {
LocalBestHeight int64 `json:"localBestHeight"`
NetBestHeight int `json:"netBestHeight"`
TimeOffset string `json:"timeOffset"`
InCount int8 `json:"inCount"`
OutCount int8 `json:"outCount"`
} `json:"data"`
}
type CmdGetVersionInfo struct {
Success bool `json:"success"`
Data struct {
MyVersion string `json:"myVersion"`
NewestVersion string `json:"newestVersion"`
NetworkVersion int `json:"networkVersion"`
Information string `json:"information"`
} `json:"data"`
}
type CmdGetBestBlockHash struct {
Success bool `json:"success"`
Data struct {
Value string `json:"value"`
} `json:"data"`
}
type CmdGetBestBlockHeight struct {
Success bool `json:"success"`
Data struct {
Value uint32 `json:"value"`
} `json:"data"`
}
type CmdTxBroadcast struct {
Success bool `json:"success"`
Data struct {
Value string `json:"value"`
} `json:"data"`
}
type CmdGetBlockHeader struct {
Success bool `json:"success"`
Data struct {
Hash string `json:"hash"`
PreHash string `json:"preHash"`
MerkleHash string `json:"merkleHash"`
StateRoot string `json:"stateRoot"`
Time int64 `json:"time"`
Height int64 `json:"height"`
TxCount int `json:"txCount"`
PackingAddress string `json:"packingAddress"`
ConfirmCount int `json:"confirmCount"`
ScriptSig string `json:"scriptSig"`
Size int `json:"size"`
Reward float64 `json:"reward"`
Fee float64 `json:"fee"`
} `json:"data"`
}
type CmdGetBlock struct {
Success bool `json:"success"`
Data struct {
Hash string `json:"hash"`
PreHash string `json:"preHash"`
MerkleHash string `json:"merkleHash"`
StateRoot string `json:"stateRoot"`
Time int64 `json:"time"`
Height int64 `json:"height"`
TxCount int `json:"txCount"`
PackingAddress string `json:"packingAddress"`
ConfirmCount int `json:"confirmCount"`
ScriptSig string `json:"scriptSig"`
Size int `json:"size"`
Reward float64 `json:"reward"`
Fee float64 `json:"fee"`
TxList []Tx `json:"txList"`
} `json:"data"`
}
type CmdGetTx struct {
Success bool `json:"success"`
Tx Tx `json:"data"`
}
type Tx struct {
Hash string `json:"hash"`
Type int `json:"type"`
Time int64 `json:"time"`
BlockHeight int64 `json:"blockHeight"`
Fee float64 `json:"fee"`
Value float64 `json:"value"`
Remark string `json:"remark"`
ScriptSig string `json:"scriptSig"`
Status int `json:"status"`
ConfirmCount int `json:"confirmCount"`
Size int `json:"size"`
Inputs []struct {
FromHash string `json:"fromHash"`
FromIndex uint32 `json:"fromIndex"`
Address string `json:"address"`
Value float64 `json:"value"`
} `json:"inputs"`
Outputs []struct {
Address string `json:"address"`
Value int64 `json:"value"`
LockTime int64 `json:"lockTime"`
} `json:"outputs"`
}
type CmdGetTxBytes struct {
Success bool `json:"success"`
Data struct {
Value string `json:"value"`
} `json:"data"`
}
func (n *NulsRPC) GetChainInfo() (*bchain.ChainInfo, error) {
networkInfo := CmdGetNetworkInfo{}
error := n.Call("/api/network/info", &networkInfo)
if error != nil {
return nil, error
}
versionInfo := CmdGetVersionInfo{}
error = n.Call("/api/client/version", &versionInfo)
if error != nil {
return nil, error
}
chainInfo := &bchain.ChainInfo{
Chain: "nuls",
Blocks: networkInfo.Data.NetBestHeight,
Headers: networkInfo.Data.NetBestHeight,
Bestblockhash: "",
Difficulty: networkInfo.Data.TimeOffset,
SizeOnDisk: networkInfo.Data.LocalBestHeight,
Version: versionInfo.Data.MyVersion,
Subversion: versionInfo.Data.NewestVersion,
ProtocolVersion: strconv.Itoa(versionInfo.Data.NetworkVersion),
Timeoffset: 0,
Warnings: versionInfo.Data.Information,
}
return chainInfo, nil
}
func (n *NulsRPC) GetBestBlockHash() (string, error) {
bestBlockHash := CmdGetBestBlockHash{}
error := n.Call("/api/block/newest/hash", &bestBlockHash)
if error != nil {
return "", error
}
return bestBlockHash.Data.Value, nil
}
func (n *NulsRPC) GetBestBlockHeight() (uint32, error) {
bestBlockHeight := CmdGetBestBlockHeight{}
error := n.Call("/api/block/newest/height", &bestBlockHeight)
if error != nil {
return 0, error
}
return bestBlockHeight.Data.Value, nil
}
func (n *NulsRPC) GetBlockHash(height uint32) (string, error) {
blockHeader := CmdGetBlockHeader{}
error := n.Call("/api/block/header/height/"+strconv.Itoa(int(height)), &blockHeader)
if error != nil {
return "", error
}
if !blockHeader.Success {
return "", bchain.ErrBlockNotFound
}
return blockHeader.Data.Hash, nil
}
func (n *NulsRPC) GetBlockHeader(hash string) (*bchain.BlockHeader, error) {
uri := "/api/block/header/hash/" + hash
return n.getBlobkHeader(uri)
}
func (n *NulsRPC) GetBlockHeaderByHeight(height uint32) (*bchain.BlockHeader, error) {
uri := "/api/block/header/height/" + strconv.Itoa(int(height))
return n.getBlobkHeader(uri)
}
func (n *NulsRPC) getBlobkHeader(uri string) (*bchain.BlockHeader, error) {
blockHeader := CmdGetBlockHeader{}
error := n.Call(uri, &blockHeader)
if error != nil {
return nil, error
}
if !blockHeader.Success {
return nil, bchain.ErrBlockNotFound
}
nexHash, _ := n.GetBlockHash(uint32(blockHeader.Data.Height + 1))
header := &bchain.BlockHeader{
Hash: blockHeader.Data.Hash,
Prev: blockHeader.Data.PreHash,
Next: nexHash,
Height: uint32(blockHeader.Data.Height),
Confirmations: blockHeader.Data.ConfirmCount,
Size: blockHeader.Data.Size,
Time: blockHeader.Data.Time / 1000,
}
return header, nil
}
func (n *NulsRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) {
url := "/api/block/hash/" + hash
if hash == "" {
url = "/api/block/height/" + strconv.Itoa(int(height))
}
getBlock := CmdGetBlock{}
error := n.Call(url, &getBlock)
if error != nil {
return nil, error
}
if !getBlock.Success {
return nil, bchain.ErrBlockNotFound
}
nexHash, _ := n.GetBlockHash(uint32(getBlock.Data.Height + 1))
header := bchain.BlockHeader{
Hash: getBlock.Data.Hash,
Prev: getBlock.Data.PreHash,
Next: nexHash,
Height: uint32(getBlock.Data.Height),
Confirmations: getBlock.Data.ConfirmCount,
Size: getBlock.Data.Size,
Time: getBlock.Data.Time / 1000,
}
var txs []bchain.Tx
for _, rawTx := range getBlock.Data.TxList {
tx, err := converTx(rawTx)
if err != nil {
return nil, err
}
tx.Blocktime = header.Time
txs = append(txs, *tx)
}
block := &bchain.Block{
BlockHeader: header,
Txs: txs,
}
return block, nil
}
func (n *NulsRPC) GetBlockInfo(hash string) (*bchain.BlockInfo, error) {
if hash == "" {
return nil, bchain.ErrBlockNotFound
}
getBlock := CmdGetBlock{}
error := n.Call("/api/block/hash/"+hash, &getBlock)
if error != nil {
return nil, error
}
if !getBlock.Success {
return nil, bchain.ErrBlockNotFound
}
nexHash, _ := n.GetBlockHash(uint32(getBlock.Data.Height + 1))
header := bchain.BlockHeader{
Hash: getBlock.Data.Hash,
Prev: getBlock.Data.PreHash,
Next: nexHash,
Height: uint32(getBlock.Data.Height),
Confirmations: getBlock.Data.ConfirmCount,
Size: getBlock.Data.Size,
Time: getBlock.Data.Time / 1000,
}
var txIds []string
for _, rawTx := range getBlock.Data.TxList {
txIds = append(txIds, rawTx.Hash)
}
blockInfo := &bchain.BlockInfo{
BlockHeader: header,
MerkleRoot: getBlock.Data.MerkleHash,
//Version: getBlock.Data.StateRoot,
Txids: txIds,
}
return blockInfo, nil
}
func (n *NulsRPC) GetMempoolTransactions() ([]string, error) {
return nil, nil
}
func (n *NulsRPC) GetTransaction(txid string) (*bchain.Tx, error) {
if txid == "" {
return nil, bchain.ErrTxidMissing
}
getTx := CmdGetTx{}
error := n.Call("/api/tx/hash/"+txid, &getTx)
if error != nil {
return nil, error
}
if !getTx.Success {
return nil, bchain.ErrTxNotFound
}
tx, err := converTx(getTx.Tx)
if err != nil {
return nil, err
}
blockHeaderHeight := getTx.Tx.BlockHeight
// shouldn't it check the error here?
blockHeader, _ := n.GetBlockHeaderByHeight(uint32(blockHeaderHeight))
if blockHeader != nil {
tx.Blocktime = blockHeader.Time
}
hexBytys, e := n.GetTransactionSpecific(tx)
if e == nil {
var hex string
json.Unmarshal(hexBytys, &hex)
tx.Hex = hex
tx.CoinSpecificData = hex
}
return tx, nil
}
func (n *NulsRPC) GetTransactionForMempool(txid string) (*bchain.Tx, error) {
return nil, nil
}
func (n *NulsRPC) GetTransactionSpecific(tx *bchain.Tx) (json.RawMessage, error) {
if tx == nil {
return nil, bchain.ErrTxNotFound
}
if csd, ok := tx.CoinSpecificData.(json.RawMessage); ok {
return csd, nil
}
getTxBytes := CmdGetTxBytes{}
error := n.Call("/api/tx/bytes?hash="+tx.Txid, &getTxBytes)
if error != nil {
return nil, error
}
if !getTxBytes.Success {
return nil, bchain.ErrTxNotFound
}
txBytes, byErr := base64.StdEncoding.DecodeString(getTxBytes.Data.Value)
if byErr != nil {
return nil, byErr
}
hexBytes := make([]byte, len(txBytes)*2)
hex.Encode(hexBytes, txBytes)
m, err := json.Marshal(string(hexBytes))
return json.RawMessage(m), err
}
func (n *NulsRPC) EstimateSmartFee(blocks int, conservative bool) (big.Int, error) {
return n.EstimateFee(blocks)
}
func (n *NulsRPC) EstimateFee(blocks int) (big.Int, error) {
return *big.NewInt(100000), nil
}
func (n *NulsRPC) SendRawTransaction(tx string) (string, error) {
broadcast := CmdTxBroadcast{}
req := struct {
TxHex string `json:"txHex"`
}{
TxHex: tx,
}
error := n.Post("/api/accountledger/transaction/broadcast", req, &broadcast)
if error != nil {
return "", error
}
if !broadcast.Success {
return "", bchain.ErrTxidMissing
}
return broadcast.Data.Value, nil
}
// Call calls Backend RPC interface, using RPCMarshaler interface to marshall the request
func (b *NulsRPC) Call(uri string, res interface{}) error {
url := b.rpcURL + uri
httpReq, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
httpReq.SetBasicAuth(b.user, b.password)
httpRes, err := b.client.Do(httpReq)
// in some cases the httpRes can contain data even if it returns error
// see http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/
if httpRes != nil {
defer httpRes.Body.Close()
}
if err != nil {
return err
}
// if server returns HTTP error code it might not return json with response
// handle both cases
if httpRes.StatusCode != 200 {
err = safeDecodeResponse(httpRes.Body, &res)
if err != nil {
return errors.Errorf("%v %v", httpRes.Status, err)
}
return nil
}
return safeDecodeResponse(httpRes.Body, &res)
}
func (b *NulsRPC) Post(uri string, req interface{}, res interface{}) error {
url := b.rpcURL + uri
httpData, err := b.RPCMarshaler.Marshal(req)
if err != nil {
return err
}
httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(httpData))
if err != nil {
return err
}
httpReq.SetBasicAuth(b.user, b.password)
httpReq.Header.Set("Content-Type", "application/json")
httpRes, err := b.client.Do(httpReq)
// in some cases the httpRes can contain data even if it returns error
// see http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/
if httpRes != nil {
defer httpRes.Body.Close()
}
if err != nil {
return err
}
// if server returns HTTP error code it might not return json with response
// handle both cases
if httpRes.StatusCode != 200 {
err = safeDecodeResponse(httpRes.Body, &res)
if err != nil {
return errors.Errorf("%v %v", httpRes.Status, err)
}
return nil
}
return safeDecodeResponse(httpRes.Body, &res)
}
func safeDecodeResponse(body io.ReadCloser, res *interface{}) (err error) {
var data []byte
defer func() {
if r := recover(); r != nil {
glog.Error("unmarshal json recovered from panic: ", r, "; data: ", string(data))
debug.PrintStack()
if len(data) > 0 && len(data) < 2048 {
err = errors.Errorf("Error: %v", string(data))
} else {
err = errors.New("Internal error")
}
}
}()
data, err = ioutil.ReadAll(body)
if err != nil {
return err
}
//fmt.Println(string(data))
error := json.Unmarshal(data, res)
return error
}
func converTx(rawTx Tx) (*bchain.Tx, error) {
var lockTime int64 = 0
var vins = make([]bchain.Vin, 0)
var vouts []bchain.Vout
for _, input := range rawTx.Inputs {
vin := bchain.Vin{
Coinbase: "",
Txid: input.FromHash,
Vout: input.FromIndex,
ScriptSig: bchain.ScriptSig{},
Sequence: 0,
Addresses: []string{input.Address},
}
vins = append(vins, vin)
}
for index, output := range rawTx.Outputs {
vout := bchain.Vout{
ValueSat: *big.NewInt(output.Value),
//JsonValue: "",
//LockTime: output.LockTime,
N: uint32(index),
ScriptPubKey: bchain.ScriptPubKey{
Hex: output.Address,
Addresses: []string{
output.Address,
},
},
}
vouts = append(vouts, vout)
if lockTime < output.LockTime {
lockTime = output.LockTime
}
}
tx := &bchain.Tx{
Hex: "",
Txid: rawTx.Hash,
Version: 0,
LockTime: uint32(lockTime),
Vin: vins,
Vout: vouts,
Confirmations: uint32(rawTx.ConfirmCount),
Time: rawTx.Time / 1000,
}
return tx, nil
}