Return inputs for socket.io getAddressHistory and getDetailedTransaction

pull/74/head
Martin Boehm 2018-10-06 23:35:03 +02:00
parent eb5781f218
commit f9809d7507
5 changed files with 223 additions and 262 deletions

View File

@ -64,21 +64,24 @@ type Vout struct {
}
type Tx struct {
Txid string `json:"txid"`
Version int32 `json:"version,omitempty"`
Locktime uint32 `json:"locktime,omitempty"`
Vin []Vin `json:"vin"`
Vout []Vout `json:"vout"`
Blockhash string `json:"blockhash,omitempty"`
Blockheight int `json:"blockheight"`
Confirmations uint32 `json:"confirmations"`
Time int64 `json:"time,omitempty"`
Blocktime int64 `json:"blocktime"`
ValueOut string `json:"valueOut"`
Size int `json:"size,omitempty"`
ValueIn string `json:"valueIn"`
Fees string `json:"fees"`
Hex string `json:"hex"`
Txid string `json:"txid"`
Version int32 `json:"version,omitempty"`
Locktime uint32 `json:"locktime,omitempty"`
Vin []Vin `json:"vin"`
Vout []Vout `json:"vout"`
Blockhash string `json:"blockhash,omitempty"`
Blockheight int `json:"blockheight"`
Confirmations uint32 `json:"confirmations"`
Time int64 `json:"time,omitempty"`
Blocktime int64 `json:"blocktime"`
ValueOut string `json:"valueOut"`
ValueOutSat big.Int `json:"-"`
Size int `json:"size,omitempty"`
ValueIn string `json:"valueIn"`
ValueInSat big.Int `json:"-"`
Fees string `json:"fees"`
FeesSat big.Int `json:"-"`
Hex string `json:"hex"`
}
type Paging struct {

View File

@ -125,6 +125,7 @@ func (w *Worker) GetTransaction(txid string, bestheight uint32, spendingTxs bool
vin.Txid = bchainVin.Txid
vin.N = i
vin.Vout = bchainVin.Vout
vin.Sequence = int64(bchainVin.Sequence)
vin.ScriptSig.Hex = bchainVin.ScriptSig.Hex
// bchainVin.Txid=="" is coinbase transaction
if bchainVin.Txid != "" {
@ -200,11 +201,14 @@ func (w *Worker) GetTransaction(txid string, bestheight uint32, spendingTxs bool
Blocktime: bchainTx.Blocktime,
Confirmations: bchainTx.Confirmations,
Fees: w.chainParser.AmountToDecimalString(&feesSat),
FeesSat: feesSat,
Locktime: bchainTx.LockTime,
Time: bchainTx.Time,
Txid: bchainTx.Txid,
ValueIn: w.chainParser.AmountToDecimalString(&valInSat),
ValueInSat: valInSat,
ValueOut: w.chainParser.AmountToDecimalString(&valOutSat),
ValueOutSat: valOutSat,
Version: bchainTx.Version,
Hex: bchainTx.Hex,
Vin: vins,

View File

@ -6,6 +6,7 @@ import (
"blockbook/common"
"blockbook/db"
"encoding/json"
"math/big"
"net/http"
"strconv"
"strings"
@ -27,10 +28,16 @@ type SocketIoServer struct {
chainParser bchain.BlockChainParser
metrics *common.Metrics
is *common.InternalState
api *api.Worker
}
// NewSocketIoServer creates new SocketIo interface to blockbook and returns its handle
func NewSocketIoServer(db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState) (*SocketIoServer, error) {
api, err := api.NewWorker(db, chain, txCache, is)
if err != nil {
return nil, err
}
server := gosocketio.NewServer(transport.GetDefaultWebsocketTransport())
server.On(gosocketio.OnConnection, func(c *gosocketio.Channel) {
@ -59,6 +66,7 @@ func NewSocketIoServer(db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCa
chainParser: chain.GetChainParser(),
metrics: metrics,
is: is,
api: api,
}
server.On("message", s.onMessage)
@ -244,33 +252,33 @@ type txOutputs struct {
Satoshis int64 `json:"satoshis"`
Script *string `json:"script"`
// ScriptAsm *string `json:"scriptAsm"`
SpentTxID *string `json:"spentTxId,omitempty"`
SpentIndex int `json:"spentIndex,omitempty"`
SpentHeight int `json:"spentHeight,omitempty"`
Address *string `json:"address"`
// SpentTxID *string `json:"spentTxId,omitempty"`
// SpentIndex int `json:"spentIndex,omitempty"`
// SpentHeight int `json:"spentHeight,omitempty"`
Address *string `json:"address"`
}
type resTx struct {
Hex string `json:"hex"`
// BlockHash string `json:"blockHash,omitempty"`
Height int `json:"height"`
BlockTimestamp int64 `json:"blockTimestamp,omitempty"`
// Version int `json:"version"`
Hash string `json:"hash"`
Locktime int `json:"locktime,omitempty"`
Height int `json:"height"`
BlockTimestamp int64 `json:"blockTimestamp,omitempty"`
Version int `json:"version"`
Hash string `json:"hash"`
Locktime int `json:"locktime,omitempty"`
// Size int `json:"size,omitempty"`
Inputs []txInputs `json:"inputs"`
// InputSatoshis int64 `json:"inputSatoshis,omitempty"`
Outputs []txOutputs `json:"outputs"`
// OutputSatoshis int64 `json:"outputSatoshis,omitempty"`
// FeeSatoshis int64 `json:"feeSatoshis,omitempty"`
Inputs []txInputs `json:"inputs"`
InputSatoshis int64 `json:"inputSatoshis,omitempty"`
Outputs []txOutputs `json:"outputs"`
OutputSatoshis int64 `json:"outputSatoshis,omitempty"`
FeeSatoshis int64 `json:"feeSatoshis,omitempty"`
}
type addressHistoryItem struct {
Addresses map[string]addressHistoryIndexes `json:"addresses"`
Satoshis int64 `json:"satoshis"`
Confirmations int `json:"confirmations"`
Tx resTx `json:"tx"`
Addresses map[string]*addressHistoryIndexes `json:"addresses"`
Satoshis int64 `json:"satoshis"`
Confirmations int `json:"confirmations"`
Tx resTx `json:"tx"`
}
type resultGetAddressHistory struct {
@ -289,20 +297,55 @@ func stringInSlice(a string, list []string) bool {
return false
}
func txToResTx(tx *bchain.Tx, height int, hi []txInputs, ho []txOutputs) resTx {
func txToResTx(tx *api.Tx) resTx {
inputs := make([]txInputs, len(tx.Vin))
for i := range tx.Vin {
vin := &tx.Vin[i]
script := vin.ScriptSig.Hex
input := txInputs{
Script: &script,
Sequence: int64(vin.Sequence),
OutputIndex: int(vin.Vout),
Satoshis: vin.ValueSat.Int64(),
}
if len(vin.Addresses) > 0 {
a := vin.Addresses[0]
input.Address = &a
}
inputs[i] = input
}
outputs := make([]txOutputs, len(tx.Vout))
for i := range tx.Vout {
vout := &tx.Vout[i]
script := vout.ScriptPubKey.Hex
output := txOutputs{
Satoshis: vout.ValueSat.Int64(),
Script: &script,
}
if len(vout.ScriptPubKey.Addresses) > 0 {
a := vout.ScriptPubKey.Addresses[0]
output.Address = &a
}
outputs[i] = output
}
var h int
if tx.Confirmations == 0 {
h = -1
} else {
h = int(tx.Blockheight)
}
return resTx{
// BlockHash: tx.BlockHash,
BlockTimestamp: tx.Blocktime,
// FeeSatoshis,
Hash: tx.Txid,
Height: height,
Hex: tx.Hex,
Inputs: hi,
// InputSatoshis,
Locktime: int(tx.LockTime),
Outputs: ho,
// OutputSatoshis,
// Version: int(tx.Version),
FeeSatoshis: tx.FeesSat.Int64(),
Hash: tx.Txid,
Height: h,
Hex: tx.Hex,
Inputs: inputs,
InputSatoshis: tx.ValueInSat.Int64(),
Locktime: int(tx.Locktime),
Outputs: outputs,
OutputSatoshis: tx.ValueOutSat.Int64(),
Version: int(tx.Version),
}
}
@ -341,54 +384,53 @@ func (s *SocketIoServer) getAddressHistory(addr []string, opts *addrOpts) (res r
txids := txr.Result
res.Result.TotalCount = len(txids)
res.Result.Items = make([]addressHistoryItem, 0)
for i, txid := range txids {
if i >= opts.From && i < opts.To {
tx, height, err := s.txCache.GetTransaction(txid, bestheight)
if err != nil {
return res, err
}
ads := make(map[string]addressHistoryIndexes)
hi := make([]txInputs, 0)
ho := make([]txOutputs, 0)
for _, vout := range tx.Vout {
aoh := vout.ScriptPubKey.Hex
ao := txOutputs{
Satoshis: vout.ValueSat.Int64(),
Script: &aoh,
}
voutAddr, err := s.getAddressesFromVout(&vout)
if err != nil {
return res, err
}
if len(voutAddr) > 0 {
ao.Address = &voutAddr[0]
}
a := addressInSlice(voutAddr, addr)
if a != "" {
hi, ok := ads[a]
if ok {
hi.OutputIndexes = append(hi.OutputIndexes, int(vout.N))
} else {
hi := addressHistoryIndexes{}
hi.InputIndexes = make([]int, 0)
hi.OutputIndexes = append(hi.OutputIndexes, int(vout.N))
ads[a] = hi
}
}
ho = append(ho, ao)
}
ahi := addressHistoryItem{}
ahi.Addresses = ads
ahi.Confirmations = int(tx.Confirmations)
var h int
if tx.Confirmations == 0 {
h = -1
} else {
h = int(height)
}
ahi.Tx = txToResTx(tx, h, hi, ho)
res.Result.Items = append(res.Result.Items, ahi)
to := len(txids)
if to > opts.To {
to = opts.To
}
for txi := opts.From; txi < to; txi++ {
tx, err := s.api.GetTransaction(txids[txi], bestheight, false)
// for i, txid := range txids {
// if i >= opts.From && i < opts.To {
// tx, err := s.api.GetTransaction(txid, bestheight, false)
if err != nil {
return res, err
}
ads := make(map[string]*addressHistoryIndexes)
var totalSat big.Int
for i := range tx.Vin {
vin := &tx.Vin[i]
a := addressInSlice(vin.Addresses, addr)
if a != "" {
hi := ads[a]
if hi == nil {
hi = &addressHistoryIndexes{OutputIndexes: []int{}}
ads[a] = hi
}
hi.InputIndexes = append(hi.InputIndexes, int(vin.N))
totalSat.Sub(&totalSat, &vin.ValueSat)
}
}
for i := range tx.Vout {
vout := &tx.Vout[i]
a := addressInSlice(vout.ScriptPubKey.Addresses, addr)
if a != "" {
hi := ads[a]
if hi == nil {
hi = &addressHistoryIndexes{InputIndexes: []int{}}
ads[a] = hi
}
hi.OutputIndexes = append(hi.OutputIndexes, int(vout.N))
totalSat.Add(&totalSat, &vout.ValueSat)
}
}
ahi := addressHistoryItem{}
ahi.Addresses = ads
ahi.Confirmations = int(tx.Confirmations)
ahi.Satoshis = totalSat.Int64()
ahi.Tx = txToResTx(tx)
res.Result.Items = append(res.Result.Items, ahi)
// }
}
return
}
@ -594,78 +636,11 @@ func (s *SocketIoServer) getDetailedTransaction(txid string) (res resultGetDetai
if err != nil {
return
}
tx, height, err := s.txCache.GetTransaction(txid, bestheight)
tx, err := s.api.GetTransaction(txid, bestheight, false)
if err != nil {
return res, err
}
hi := make([]txInputs, 0)
ho := make([]txOutputs, 0)
for _, vin := range tx.Vin {
ais := vin.ScriptSig.Hex
ai := txInputs{
Script: &ais,
Sequence: int64(vin.Sequence),
OutputIndex: int(vin.Vout),
}
if vin.Txid != "" {
var voutAddr []string
// load spending addresses from TxAddresses
ta, err := s.db.GetTxAddresses(vin.Txid)
if err != nil {
return res, err
}
if ta == nil {
// the tx may be in mempool, try to load it from backend
otx, _, err := s.txCache.GetTransaction(vin.Txid, bestheight)
if err != nil {
return res, err
}
if len(otx.Vout) > int(vin.Vout) {
vout := &otx.Vout[vin.Vout]
voutAddr, err = s.getAddressesFromVout(vout)
if err != nil {
return res, err
}
ai.Satoshis = vout.ValueSat.Int64()
}
} else {
if len(ta.Outputs) > int(vin.Vout) {
output := &ta.Outputs[vin.Vout]
ai.Satoshis = output.ValueSat.Int64()
voutAddr, _, err = output.Addresses(s.chainParser)
if err != nil {
return res, err
}
}
}
if len(voutAddr) > 0 {
ai.Address = &voutAddr[0]
}
}
hi = append(hi, ai)
}
for _, vout := range tx.Vout {
aos := vout.ScriptPubKey.Hex
ao := txOutputs{
Satoshis: vout.ValueSat.Int64(),
Script: &aos,
}
voutAddr, err := s.getAddressesFromVout(&vout)
if err != nil {
return res, err
}
if len(voutAddr) > 0 {
ao.Address = &voutAddr[0]
}
ho = append(ho, ao)
}
var h int
if tx.Confirmations == 0 {
h = -1
} else {
h = int(height)
}
res.Result = txToResTx(tx, h, hi, ho)
res.Result = txToResTx(tx)
return
}

View File

@ -1,4 +1,4 @@
// +build integration
// build integration
package server
@ -8,6 +8,7 @@ import (
"encoding/json"
"flag"
"os"
"reflect"
"sort"
"strings"
"testing"
@ -94,16 +95,74 @@ func getFullAddressHistory(addr []string, rr addrOpts, ws *gosocketio.Client) (*
return &bbResponse, nil
}
func equalAddressHistoryItem(logItem addressHistoryItem, bbItem addressHistoryItem) error {
if logItem.Tx.Hash != bbItem.Tx.Hash {
return errors.Errorf("Different hash bb: %v log: %v", bbItem.Tx.Hash, logItem.Tx.Hash)
func equalTx(logTx resTx, bbTx resTx) error {
if logTx.Hash != bbTx.Hash {
return errors.Errorf("Different Hash bb: %v log: %v", bbTx.Hash, logTx.Hash)
}
if logItem.Tx.Hex != bbItem.Tx.Hex {
return errors.Errorf("Different hex bb: %v log: %v", bbItem.Tx.Hex, logItem.Tx.Hex)
if logTx.Hex != bbTx.Hex {
return errors.Errorf("Different Hex bb: %v log: %v", bbTx.Hex, logTx.Hex)
}
if logTx.BlockTimestamp != bbTx.BlockTimestamp {
return errors.Errorf("Different BlockTimestamp bb: %v log: %v", bbTx.BlockTimestamp, logTx.BlockTimestamp)
}
if logTx.FeeSatoshis != bbTx.FeeSatoshis {
return errors.Errorf("Different FeeSatoshis bb: %v log: %v", bbTx.FeeSatoshis, logTx.FeeSatoshis)
}
if logTx.Height != bbTx.Height {
return errors.Errorf("Different Height bb: %v log: %v", bbTx.Height, logTx.Height)
}
if logTx.InputSatoshis != bbTx.InputSatoshis {
return errors.Errorf("Different InputSatoshis bb: %v log: %v", bbTx.InputSatoshis, logTx.InputSatoshis)
}
if logTx.Locktime != bbTx.Locktime {
return errors.Errorf("Different Locktime bb: %v log: %v", bbTx.Locktime, logTx.Locktime)
}
if logTx.OutputSatoshis != bbTx.OutputSatoshis {
return errors.Errorf("Different OutputSatoshis bb: %v log: %v", bbTx.OutputSatoshis, logTx.OutputSatoshis)
}
if logTx.Version != bbTx.Version {
return errors.Errorf("Different Version bb: %v log: %v", bbTx.Version, logTx.Version)
}
if len(logTx.Inputs) != len(bbTx.Inputs) {
return errors.Errorf("Different number of Inputs bb: %v log: %v", len(bbTx.Inputs), len(logTx.Inputs))
}
// blockbook parses bech addresses, it is ok for bitcore to return nil address and blockbook parsed address
for i := range logTx.Inputs {
if logTx.Inputs[i].Satoshis != bbTx.Inputs[i].Satoshis ||
(bbTx.Inputs[i].Address == nil && logTx.Inputs[i].Address != bbTx.Inputs[i].Address) ||
(logTx.Inputs[i].Address != nil && *logTx.Inputs[i].Address != *bbTx.Inputs[i].Address) ||
logTx.Inputs[i].OutputIndex != bbTx.Inputs[i].OutputIndex ||
logTx.Inputs[i].Sequence != bbTx.Inputs[i].Sequence {
return errors.Errorf("Different Inputs bb: %+v log: %+v", bbTx.Inputs, logTx.Inputs)
}
}
if len(logTx.Outputs) != len(bbTx.Outputs) {
return errors.Errorf("Different number of Outputs bb: %v log: %v", len(bbTx.Outputs), len(logTx.Outputs))
}
// blockbook parses bech addresses, it is ok for bitcore to return nil address and blockbook parsed address
for i := range logTx.Outputs {
if logTx.Outputs[i].Satoshis != bbTx.Outputs[i].Satoshis ||
(bbTx.Outputs[i].Address == nil && logTx.Outputs[i].Address != bbTx.Outputs[i].Address) ||
(logTx.Outputs[i].Address != nil && *logTx.Outputs[i].Address != *bbTx.Outputs[i].Address) {
return errors.Errorf("Different Outputs bb: %+v log: %+v", bbTx.Outputs, logTx.Outputs)
}
}
// Addresses do not match, bb getAddressHistory does not return input addresses
return nil
}
func equalAddressHistoryItem(logItem addressHistoryItem, bbItem addressHistoryItem) error {
if err := equalTx(logItem.Tx, bbItem.Tx); err != nil {
return err
}
if !reflect.DeepEqual(logItem.Addresses, bbItem.Addresses) {
return errors.Errorf("Different Addresses bb: %v log: %v", bbItem.Addresses, logItem.Addresses)
}
if logItem.Satoshis != bbItem.Satoshis {
return errors.Errorf("Different Satoshis bb: %v log: %v", bbItem.Satoshis, logItem.Satoshis)
}
return nil
}
func verifyGetAddressHistory(t *testing.T, id int, lrs *logRequestResponse, bbResStr string, stat *verifyStats, ws *gosocketio.Client, bbRequest map[string]json.RawMessage) {
bbResponse := resultGetAddressHistory{}
logResponse := resultGetAddressHistory{}
@ -146,16 +205,17 @@ func verifyGetAddressHistory(t *testing.T, id int, lrs *logRequestResponse, bbRe
}
found := false
for _, bbFullItem := range bbFullResponse.Result.Items {
if err1 = equalAddressHistoryItem(logItem, bbFullItem); err1 == nil {
err1 = equalAddressHistoryItem(logItem, bbFullItem)
if err1 == nil {
found = true
break
}
if err1.Error()[:14] != "Different Hash" {
t.Log(err1)
}
}
if !found {
t.Log("getAddressHistory", id, "addresses", addr, "mismatch ", err)
// bf, _ := json.Marshal(bbFullResponse.Result)
// bl, _ := json.Marshal(logResponse.Result)
// t.Log("{ \"bf\":", string(bf), ",\"bl\":", string(bl), "}")
return
}
}
@ -237,94 +297,7 @@ func verifyGetDetailedTransaction(t *testing.T, id int, lrs *logRequestResponse,
if err := unmarshalResponses(t, id, lrs, bbResStr, &bbResponse, &logResponse); err != nil {
return
}
equalInputs := func() error {
if len(bbResponse.Result.Inputs) != len(logResponse.Result.Inputs) {
return errors.Errorf("mismatch number of inputs %v %v", len(bbResponse.Result.Inputs), len(logResponse.Result.Inputs))
}
for i, bbi := range bbResponse.Result.Inputs {
li := logResponse.Result.Inputs[i]
if bbi.OutputIndex != li.OutputIndex ||
bbi.Sequence != li.Sequence ||
bbi.Satoshis != li.Satoshis {
return errors.Errorf("mismatch input %v %v, %v %v, %v %v", bbi.OutputIndex, li.OutputIndex, bbi.Sequence, li.Sequence, bbi.Satoshis, li.Satoshis)
}
// both must be null or both must not be null
if bbi.Address != nil && li.Address != nil {
if *bbi.Address != *li.Address {
return errors.Errorf("mismatch input Address %v %v", *bbi.Address, *li.Address)
}
} else if bbi.Address != li.Address {
// bitcore does not parse bech P2WPKH and P2WSH addresses
if bbi.Address == nil || (*bbi.Address)[0:3] != "bc1" {
return errors.Errorf("mismatch input Address %v %v", bbi.Address, li.Address)
}
}
// both must be null or both must not be null
if bbi.Script != nil && li.Script != nil {
if *bbi.Script != *li.Script {
return errors.Errorf("mismatch input Script %v %v", *bbi.Script, *li.Script)
}
} else if bbi.Script != li.Script {
return errors.Errorf("mismatch input Script %v %v", bbi.Script, li.Script)
}
}
return nil
}
equalOutputs := func() error {
if len(bbResponse.Result.Outputs) != len(logResponse.Result.Outputs) {
return errors.Errorf("mismatch number of outputs %v %v", len(bbResponse.Result.Outputs), len(logResponse.Result.Outputs))
}
for i, bbo := range bbResponse.Result.Outputs {
lo := logResponse.Result.Outputs[i]
if bbo.Satoshis != lo.Satoshis {
return errors.Errorf("mismatch output Satoshis %v %v", bbo.Satoshis, lo.Satoshis)
}
// both must be null or both must not be null
if bbo.Script != nil && lo.Script != nil {
if *bbo.Script != *lo.Script {
return errors.Errorf("mismatch output Script %v %v", *bbo.Script, *lo.Script)
}
} else if bbo.Script != lo.Script {
return errors.Errorf("mismatch output Script %v %v", bbo.Script, lo.Script)
}
// both must be null or both must not be null
if bbo.Address != nil && lo.Address != nil {
if *bbo.Address != *lo.Address {
return errors.Errorf("mismatch output Address %v %v", *bbo.Address, *lo.Address)
}
} else if bbo.Address != lo.Address {
// bitcore does not parse bech P2WPKH and P2WSH addresses
if bbo.Address == nil || (*bbo.Address)[0:3] != "bc1" {
return errors.Errorf("mismatch output Address %v %v", bbo.Address, lo.Address)
}
}
}
return nil
}
// the tx in the log could have been still in mempool with Height -1
if (bbResponse.Result.Height != logResponse.Result.Height && logResponse.Result.Height != -1) ||
bbResponse.Result.Hash != logResponse.Result.Hash {
t.Log("getDetailedTransaction", id, "mismatch bb:", bbResponse.Result.Hash, bbResponse.Result.Height,
"log:", logResponse.Result.Hash, logResponse.Result.Height)
return
}
// the tx in the log could have been still in mempool with BlockTimestamp 0
if bbResponse.Result.BlockTimestamp != logResponse.Result.BlockTimestamp && logResponse.Result.BlockTimestamp != 0 {
t.Log("getDetailedTransaction", id, "mismatch BlockTimestamp:", bbResponse.Result.BlockTimestamp,
"log:", logResponse.Result.BlockTimestamp)
return
}
if bbResponse.Result.Hex != logResponse.Result.Hex {
t.Log("getDetailedTransaction", id, "mismatch Hex:", bbResponse.Result.Hex,
"log:", logResponse.Result.Hex)
return
}
if err := equalInputs(); err != nil {
t.Log("getDetailedTransaction", id, err)
return
}
if err := equalOutputs(); err != nil {
if err := equalTx(logResponse.Result, bbResponse.Result); err != nil {
t.Log("getDetailedTransaction", id, err)
return
}

View File

@ -55,7 +55,9 @@
var addresses = document.getElementById('getAddressHistoryAddresses').value.split(",");
addresses = addresses.map(s => s.trim());
var mempool = document.getElementById("getAddressHistoryMempool").checked;
lookupAddressHistories(addresses, 0, 5, mempool, 20000000, 0, function (result) {
var from = parseInt(document.getElementById("getAddressHistoryFrom").value);
var to = parseInt(document.getElementById("getAddressHistoryTo").value);
lookupAddressHistories(addresses, from, to, mempool, 90000000, 0, function (result) {
console.log('getAddressHistory sent successfully');
console.log(result);
document.getElementById('getAddressHistoryResult').innerText = JSON.stringify(result).replace(/,/g, ", ");
@ -283,7 +285,11 @@
<input class="btn btn-secondary" type="button" value="getAddressHistory" onclick="getAddressHistory()">
</div>
<div class="col-8">
<input type="text" class="form-control" id="getAddressHistoryAddresses" value="2N4Q5FhU2497BryFfUgbqkAJE87aKHUhXMp,2Mt7P2BAfE922zmfXrdcYTLyR7GUvbwSEns">
<div class="row" style="margin: 0;">
<input type="text" style="width: 84%" class="form-control" id="getAddressHistoryAddresses" value="2N4Q5FhU2497BryFfUgbqkAJE87aKHUhXMp,2Mt7P2BAfE922zmfXrdcYTLyR7GUvbwSEns">
<input type="text" style="width: 7%; margin-left: 5px; margin-right: 5px;" class="form-control" id="getAddressHistoryFrom" value="0">
<input type="text" style="width: 7%" class="form-control" id="getAddressHistoryTo" value="5">
</div>
</div>
<div class="col form-inline">
<input type="checkbox" id="getAddressHistoryMempool">&nbsp;