629 lines
15 KiB
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
|
|
}
|