Keep api v1 as compatible with blockbook 0.1.x, add api v2

ethereum
Martin Boehm 2018-12-13 14:31:34 +01:00
parent 9c142663ce
commit e9e6b472b6
8 changed files with 345 additions and 40 deletions

View File

@ -52,6 +52,26 @@ func (a *Amount) MarshalJSON() (out []byte, err error) {
return []byte(`"` + (*big.Int)(a).String() + `"`), nil
}
func (a *Amount) String() string {
if a == nil {
return ""
}
return (*big.Int)(a).String()
}
// DecimalString returns amount with decimal point placed according to parameter d
func (a *Amount) DecimalString(d int) string {
return bchain.AmountToDecimalString((*big.Int)(a), d)
}
// AsBigInt returns big.Int type for the Amount (empty if Amount is nil)
func (a *Amount) AsBigInt() big.Int {
if a == nil {
return *new(big.Int)
}
return big.Int(*a)
}
// ScriptSig contains input script
type ScriptSig struct {
Hex string `json:"hex"`
@ -135,8 +155,7 @@ type Tx struct {
Confirmations uint32 `json:"confirmations"`
Time int64 `json:"time,omitempty"`
Blocktime int64 `json:"blocktime"`
ValueOut string `json:"valueOut"`
ValueOutSat *Amount `json:"-"`
ValueOutSat *Amount `json:"valueOut,omitempty"`
Size int `json:"size,omitempty"`
ValueInSat *Amount `json:"valueIn,omitempty"`
FeesSat *Amount `json:"fees,omitempty"`
@ -219,6 +238,7 @@ type BlockbookInfo struct {
InSyncMempool bool `json:"inSyncMempool"`
LastMempoolTime time.Time `json:"lastMempoolTime"`
MempoolSize int `json:"mempoolSize"`
Decimals int `json:"decimals"`
DbSize int64 `json:"dbSize"`
DbSizeFromColumns int64 `json:"dbSizeFromColumns,omitempty"`
DbColumns []common.InternalStateColumn `json:"dbColumns,omitempty"`

194
api/typesv1.go 100644
View File

@ -0,0 +1,194 @@
package api
import (
"blockbook/bchain"
"math/big"
)
// VinV1 is used for legacy api v1
type VinV1 struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
Sequence int64 `json:"sequence,omitempty"`
N int `json:"n"`
ScriptSig ScriptSig `json:"scriptSig"`
AddrDesc bchain.AddressDescriptor `json:"-"`
Addresses []string `json:"addresses"`
Searchable bool `json:"-"`
Value string `json:"value"`
ValueSat big.Int `json:"-"`
}
// VoutV1 is used for legacy api v1
type VoutV1 struct {
Value string `json:"value"`
ValueSat big.Int `json:"-"`
N int `json:"n"`
ScriptPubKey ScriptPubKey `json:"scriptPubKey"`
Spent bool `json:"spent"`
SpentTxID string `json:"spentTxId,omitempty"`
SpentIndex int `json:"spentIndex,omitempty"`
SpentHeight int `json:"spentHeight,omitempty"`
}
// TxV1 is used for legacy api v1
type TxV1 struct {
Txid string `json:"txid"`
Version int32 `json:"version,omitempty"`
Locktime uint32 `json:"locktime,omitempty"`
Vin []VinV1 `json:"vin"`
Vout []VoutV1 `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"`
}
// AddressV1 is used for legacy api v1
type AddressV1 struct {
Paging
AddrStr string `json:"addrStr"`
Balance string `json:"balance"`
TotalReceived string `json:"totalReceived"`
TotalSent string `json:"totalSent"`
UnconfirmedBalance string `json:"unconfirmedBalance"`
UnconfirmedTxApperances int `json:"unconfirmedTxApperances"`
TxApperances int `json:"txApperances"`
Transactions []*TxV1 `json:"txs,omitempty"`
Txids []string `json:"transactions,omitempty"`
}
// AddressUtxoV1 is used for legacy api v1
type AddressUtxoV1 struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
Amount string `json:"amount"`
AmountSat big.Int `json:"satoshis"`
Height int `json:"height,omitempty"`
Confirmations int `json:"confirmations"`
}
// BlockV1 contains information about block
type BlockV1 struct {
Paging
bchain.BlockInfo
TxCount int `json:"TxCount"`
Transactions []*TxV1 `json:"txs,omitempty"`
}
// TxToV1 converts Tx to TxV1
func (w *Worker) TxToV1(tx *Tx) *TxV1 {
d := w.chainParser.AmountDecimals()
vinV1 := make([]VinV1, len(tx.Vin))
for i := range tx.Vin {
v := &tx.Vin[i]
vinV1[i] = VinV1{
AddrDesc: v.AddrDesc,
Addresses: v.Addresses,
N: v.N,
ScriptSig: v.ScriptSig,
Searchable: v.Searchable,
Sequence: v.Sequence,
Txid: v.Txid,
Value: v.ValueSat.DecimalString(d),
ValueSat: v.ValueSat.AsBigInt(),
Vout: v.Vout,
}
}
voutV1 := make([]VoutV1, len(tx.Vout))
for i := range tx.Vout {
v := &tx.Vout[i]
voutV1[i] = VoutV1{
N: v.N,
ScriptPubKey: v.ScriptPubKey,
Spent: v.Spent,
SpentHeight: v.SpentHeight,
SpentIndex: v.SpentIndex,
SpentTxID: v.SpentTxID,
Value: v.ValueSat.DecimalString(d),
ValueSat: v.ValueSat.AsBigInt(),
}
}
return &TxV1{
Blockhash: tx.Blockhash,
Blockheight: tx.Blockheight,
Blocktime: tx.Blocktime,
Confirmations: tx.Confirmations,
Fees: tx.FeesSat.DecimalString(d),
FeesSat: tx.FeesSat.AsBigInt(),
Hex: tx.Hex,
Locktime: tx.Locktime,
Size: tx.Size,
Time: tx.Time,
Txid: tx.Txid,
ValueIn: tx.ValueInSat.DecimalString(d),
ValueInSat: tx.ValueInSat.AsBigInt(),
ValueOut: tx.ValueOutSat.DecimalString(d),
ValueOutSat: tx.ValueOutSat.AsBigInt(),
Version: tx.Version,
Vin: vinV1,
Vout: voutV1,
}
}
func (w *Worker) transactionsToV1(txs []*Tx) []*TxV1 {
v1 := make([]*TxV1, len(txs))
for i := range txs {
v1[i] = w.TxToV1(txs[i])
}
return v1
}
// AddressToV1 converts Address to AddressV1
func (w *Worker) AddressToV1(a *Address) *AddressV1 {
d := w.chainParser.AmountDecimals()
return &AddressV1{
AddrStr: a.AddrStr,
Balance: a.BalanceSat.DecimalString(d),
Paging: a.Paging,
TotalReceived: a.TotalReceivedSat.DecimalString(d),
TotalSent: a.TotalSentSat.DecimalString(d),
Transactions: w.transactionsToV1(a.Transactions),
TxApperances: a.TxApperances,
Txids: a.Txids,
UnconfirmedBalance: a.UnconfirmedBalanceSat.DecimalString(d),
UnconfirmedTxApperances: a.UnconfirmedTxApperances,
}
}
// AddressUtxoToV1 converts []AddressUtxo to []AddressUtxoV1
func (w *Worker) AddressUtxoToV1(au []AddressUtxo) []AddressUtxoV1 {
d := w.chainParser.AmountDecimals()
v1 := make([]AddressUtxoV1, len(au))
for i := range au {
utxo := &au[i]
v1[i] = AddressUtxoV1{
AmountSat: utxo.AmountSat.AsBigInt(),
Amount: utxo.AmountSat.DecimalString(d),
Confirmations: utxo.Confirmations,
Height: utxo.Height,
Txid: utxo.Txid,
Vout: uint32(utxo.Vout),
}
}
return v1
}
// BlockToV1 converts Address to Address1
func (w *Worker) BlockToV1(b *Block) *BlockV1 {
return &BlockV1{
BlockInfo: b.BlockInfo,
Paging: b.Paging,
Transactions: w.transactionsToV1(b.Transactions),
TxCount: b.TxCount,
}
}

View File

@ -918,6 +918,7 @@ func (w *Worker) GetSystemInfo(internal bool) (*SystemInfo, error) {
InSyncMempool: ms,
LastMempoolTime: mt,
MempoolSize: msz,
Decimals: w.chainParser.AmountDecimals(),
DbSize: w.db.DatabaseSizeOnDisk(),
DbSizeFromColumns: dbs,
DbColumns: dbc,

View File

@ -86,6 +86,11 @@ func (p *BaseParser) AmountToDecimalString(a *big.Int) string {
return AmountToDecimalString(a, p.AmountDecimalPoint)
}
// AmountDecimals returns number of decimal places in amounts
func (p *BaseParser) AmountDecimals() int {
return p.AmountDecimalPoint
}
// ParseTxFromJson parses JSON message containing transaction and returns Tx struct
func (p *BaseParser) ParseTxFromJson(msg json.RawMessage) (*Tx, error) {
var tx Tx

View File

@ -227,6 +227,8 @@ type BlockChainParser interface {
// KeepBlockAddresses returns number of blocks which are to be kept in blockTxs column
// to be used for rollbacks
KeepBlockAddresses() int
// AmountDecimals returns number of decimal places in coin amounts
AmountDecimals() int
// AmountToDecimalString converts amount in big.Int to string with decimal point in the correct place
AmountToDecimalString(a *big.Int) string
// AmountToBigInt converts amount in json.Number (string) to big.Int

View File

@ -26,6 +26,12 @@ const txsOnPage = 25
const blocksOnPage = 50
const txsInAPI = 1000
const (
_ = iota
apiV1
apiV2
)
// PublicServer is a handle to public http server
type PublicServer struct {
binding string
@ -97,7 +103,7 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch
// default handler
serveMux.HandleFunc(path, s.htmlTemplateHandler(s.explorerIndex))
// default API handler
serveMux.HandleFunc(path+"api/", s.jsonHandler(s.apiIndex))
serveMux.HandleFunc(path+"api/", s.jsonHandler(s.apiIndex, apiV2))
return s, nil
}
@ -134,14 +140,42 @@ func (s *PublicServer) ConnectFullPublicInterface() {
serveMux.HandleFunc(path+"address/", s.addressRedirect)
}
// API calls
serveMux.HandleFunc(path+"api/block-index/", s.jsonHandler(s.apiBlockIndex))
serveMux.HandleFunc(path+"api/tx/", s.jsonHandler(s.apiTx))
serveMux.HandleFunc(path+"api/tx-specific/", s.jsonHandler(s.apiTxSpecific))
serveMux.HandleFunc(path+"api/address/", s.jsonHandler(s.apiAddress))
serveMux.HandleFunc(path+"api/utxo/", s.jsonHandler(s.apiAddressUtxo))
serveMux.HandleFunc(path+"api/block/", s.jsonHandler(s.apiBlock))
serveMux.HandleFunc(path+"api/sendtx/", s.jsonHandler(s.apiSendTx))
serveMux.HandleFunc(path+"api/estimatefee/", s.jsonHandler(s.apiEstimateFee))
// default api without version can be changed to different version at any time
// use versioned api for stability
var apiDefault int
// ethereum supports only api V2
if s.chainParser.GetChainType() == bchain.ChainEthereumType {
apiDefault = apiV2
} else {
apiDefault = apiV1
// legacy v1 format
serveMux.HandleFunc(path+"api/v1/block-index/", s.jsonHandler(s.apiBlockIndex, apiV1))
serveMux.HandleFunc(path+"api/v1/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV1))
serveMux.HandleFunc(path+"api/v1/tx/", s.jsonHandler(s.apiTx, apiV1))
serveMux.HandleFunc(path+"api/v1/address/", s.jsonHandler(s.apiAddress, apiV1))
serveMux.HandleFunc(path+"api/v1/utxo/", s.jsonHandler(s.apiAddressUtxo, apiV1))
serveMux.HandleFunc(path+"api/v1/block/", s.jsonHandler(s.apiBlock, apiV1))
serveMux.HandleFunc(path+"api/v1/sendtx/", s.jsonHandler(s.apiSendTx, apiV1))
serveMux.HandleFunc(path+"api/v1/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV1))
}
serveMux.HandleFunc(path+"api/block-index/", s.jsonHandler(s.apiBlockIndex, apiDefault))
serveMux.HandleFunc(path+"api/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiDefault))
serveMux.HandleFunc(path+"api/tx/", s.jsonHandler(s.apiTx, apiDefault))
serveMux.HandleFunc(path+"api/address/", s.jsonHandler(s.apiAddress, apiDefault))
serveMux.HandleFunc(path+"api/utxo/", s.jsonHandler(s.apiAddressUtxo, apiDefault))
serveMux.HandleFunc(path+"api/block/", s.jsonHandler(s.apiBlock, apiDefault))
serveMux.HandleFunc(path+"api/sendtx/", s.jsonHandler(s.apiSendTx, apiDefault))
serveMux.HandleFunc(path+"api/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiDefault))
// v2 format
serveMux.HandleFunc(path+"api/v2/block-index/", s.jsonHandler(s.apiBlockIndex, apiV2))
serveMux.HandleFunc(path+"api/v2/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV2))
serveMux.HandleFunc(path+"api/v2/tx/", s.jsonHandler(s.apiTx, apiV2))
serveMux.HandleFunc(path+"api/v2/address/", s.jsonHandler(s.apiAddress, apiV2))
serveMux.HandleFunc(path+"api/v2/utxo/", s.jsonHandler(s.apiAddressUtxo, apiV2))
serveMux.HandleFunc(path+"api/v2/block/", s.jsonHandler(s.apiBlock, apiV2))
serveMux.HandleFunc(path+"api/v2/sendtx/", s.jsonHandler(s.apiSendTx, apiV2))
serveMux.HandleFunc(path+"api/v2/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV2))
// socket.io interface
serveMux.Handle(path+"socket.io/", s.socketio.GetHandler())
// websocket interface
@ -204,7 +238,7 @@ func getFunctionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
func (s *PublicServer) jsonHandler(handler func(r *http.Request) (interface{}, error)) func(w http.ResponseWriter, r *http.Request) {
func (s *PublicServer) jsonHandler(handler func(r *http.Request, apiVersion int) (interface{}, error), apiVersion int) func(w http.ResponseWriter, r *http.Request) {
type jsonError struct {
Text string `json:"error"`
HTTPStatus int `json:"-"`
@ -228,7 +262,7 @@ func (s *PublicServer) jsonHandler(handler func(r *http.Request) (interface{}, e
}
json.NewEncoder(w).Encode(data)
}()
data, err = handler(r)
data, err = handler(r, apiVersion)
if err != nil || data == nil {
if apiErr, ok := err.(*api.APIError); ok {
if apiErr.Public {
@ -415,7 +449,7 @@ func formatAmountWithDecimals(a *api.Amount, d int) string {
if a == nil {
return "0"
}
return bchain.AmountToDecimalString((*big.Int)(a), d)
return a.DecimalString(d)
}
// called from template to support txdetail.html functionality
@ -659,12 +693,12 @@ func getPagingRange(page int, total int) ([]int, int, int) {
return r, pp, np
}
func (s *PublicServer) apiIndex(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiIndex(r *http.Request, apiVersion int) (interface{}, error) {
s.metrics.ExplorerViews.With(common.Labels{"action": "api-index"}).Inc()
return s.api.GetSystemInfo(false)
}
func (s *PublicServer) apiBlockIndex(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiBlockIndex(r *http.Request, apiVersion int) (interface{}, error) {
type resBlockIndex struct {
BlockHash string `json:"blockHash"`
}
@ -690,7 +724,7 @@ func (s *PublicServer) apiBlockIndex(r *http.Request) (interface{}, error) {
}, nil
}
func (s *PublicServer) apiTx(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiTx(r *http.Request, apiVersion int) (interface{}, error) {
var tx *api.Tx
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx"}).Inc()
@ -705,11 +739,14 @@ func (s *PublicServer) apiTx(r *http.Request) (interface{}, error) {
}
}
tx, err = s.api.GetTransaction(txid, spendingTxs, false)
if err == nil && apiVersion == apiV1 {
return s.api.TxToV1(tx), nil
}
}
return tx, err
}
func (s *PublicServer) apiTxSpecific(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiTxSpecific(r *http.Request, apiVersion int) (interface{}, error) {
var tx json.RawMessage
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx-specific"}).Inc()
@ -720,7 +757,7 @@ func (s *PublicServer) apiTxSpecific(r *http.Request) (interface{}, error) {
return tx, err
}
func (s *PublicServer) apiAddress(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiAddress(r *http.Request, apiVersion int) (interface{}, error) {
var address *api.Address
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc()
@ -730,11 +767,14 @@ func (s *PublicServer) apiAddress(r *http.Request) (interface{}, error) {
page = 0
}
address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsInAPI, api.TxidHistory, api.AddressFilterNone)
if err == nil && apiVersion == apiV1 {
return s.api.AddressToV1(address), nil
}
}
return address, err
}
func (s *PublicServer) apiAddressUtxo(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiAddressUtxo(r *http.Request, apiVersion int) (interface{}, error) {
var utxo []api.AddressUtxo
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc()
@ -748,11 +788,14 @@ func (s *PublicServer) apiAddressUtxo(r *http.Request) (interface{}, error) {
}
}
utxo, err = s.api.GetAddressUtxo(r.URL.Path[i+1:], onlyConfirmed)
if err == nil && apiVersion == apiV1 {
return s.api.AddressUtxoToV1(utxo), nil
}
}
return utxo, err
}
func (s *PublicServer) apiBlock(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiBlock(r *http.Request, apiVersion int) (interface{}, error) {
var block *api.Block
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "api-block"}).Inc()
@ -762,6 +805,9 @@ func (s *PublicServer) apiBlock(r *http.Request) (interface{}, error) {
page = 0
}
block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsInAPI)
if err == nil && apiVersion == apiV1 {
return s.api.BlockToV1(block), nil
}
}
return block, err
}
@ -770,7 +816,7 @@ type resultSendTransaction struct {
Result string `json:"result"`
}
func (s *PublicServer) apiSendTx(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiSendTx(r *http.Request, apiVersion int) (interface{}, error) {
var err error
var res resultSendTransaction
var hex string
@ -800,7 +846,7 @@ type resultEstimateFeeAsString struct {
Result string `json:"result"`
}
func (s *PublicServer) apiEstimateFee(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiEstimateFee(r *http.Request, apiVersion int) (interface{}, error) {
var res resultEstimateFeeAsString
s.metrics.ExplorerViews.With(common.Labels{"action": "api-estimatefee"}).Inc()
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {

View File

@ -131,7 +131,7 @@ func newPostRequest(u string, body string) *http.Request {
return r
}
func httpTests(t *testing.T, ts *httptest.Server) {
func httpTests_BitcoinType(t *testing.T, ts *httptest.Server) {
tests := []struct {
name string
r *http.Request
@ -359,6 +359,7 @@ func httpTests(t *testing.T, ts *httptest.Server) {
body: []string{
`{"blockbook":{"coin":"Fakecoin"`,
`"bestHeight":225494`,
`"decimals":8`,
`"backend":{"chain":"fakecoin","blocks":2,"headers":2,"bestblockhash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6"`,
`"version":"001001","subversion":"/Fakecoin:0.0.1/"`,
},
@ -373,17 +374,35 @@ func httpTests(t *testing.T, ts *httptest.Server) {
},
},
{
name: "apiTx",
r: newGetRequest(ts.URL + "/api/tx/05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07"),
name: "apiTx v1",
r: newGetRequest(ts.URL + "/api/v1/tx/05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07"),
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`{"txid":"05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07","vin":[{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":2,"n":0,"scriptSig":{"hex":""},"addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"],"value":"0.00009876"}],"vout":[{"value":"0.00009","n":0,"scriptPubKey":{"hex":"a914e921fc4912a315078f370d959f2c4f7b6d2a683c87","addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"]},"spent":false}],"blockhash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockheight":225494,"confirmations":1,"time":22549400002,"blocktime":22549400002,"valueOut":"0.00009","valueIn":"0.00009876","fees":"0.00000876"}`,
`{"txid":"05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07","vin":[{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":2,"n":0,"scriptSig":{"hex":""},"addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"],"value":"0.00009876"}],"vout":[{"value":"0.00009","n":0,"scriptPubKey":{"hex":"a914e921fc4912a315078f370d959f2c4f7b6d2a683c87","addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"]},"spent":false}],"blockhash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockheight":225494,"confirmations":1,"time":22549400002,"blocktime":22549400002,"valueOut":"0.00009","valueIn":"0.00009876","fees":"0.00000876","hex":""}`,
},
},
{
name: "apiTx - not found",
r: newGetRequest(ts.URL + "/api/tx/1232e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07"),
name: "apiTx - not found v1",
r: newGetRequest(ts.URL + "/api/v1/tx/1232e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07"),
status: http.StatusBadRequest,
contentType: "application/json; charset=utf-8",
body: []string{
`{"error":"Tx not found, Not found"}`,
},
},
{
name: "apiTx v2",
r: newGetRequest(ts.URL + "/api/v2/tx/05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07"),
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`{"txid":"05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07","vin":[{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":2,"n":0,"scriptSig":{"hex":""},"addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"],"value":"9876"}],"vout":[{"value":"9000","n":0,"scriptPubKey":{"hex":"a914e921fc4912a315078f370d959f2c4f7b6d2a683c87","addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"]},"spent":false}],"blockhash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockheight":225494,"confirmations":1,"time":22549400002,"blocktime":22549400002,"valueOut":"9000","valueIn":"9876","fees":"876"}`,
},
},
{
name: "apiTx - not found v2",
r: newGetRequest(ts.URL + "/api/v2/tx/1232e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07"),
status: http.StatusBadRequest,
contentType: "application/json; charset=utf-8",
body: []string{
@ -400,23 +419,41 @@ func httpTests(t *testing.T, ts *httptest.Server) {
},
},
{
name: "apiAddress",
r: newGetRequest(ts.URL + "/api/address/mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"),
name: "apiAddress v1",
r: newGetRequest(ts.URL + "/api/v1/address/mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"),
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`{"page":1,"totalPages":1,"itemsOnPage":1000,"addrStr":"mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw","balance":"0","balanceSat":"0","totalReceived":"12345.67890123","totalSent":"12345.67890123","unconfirmedBalance":"0","unconfirmedBalanceSat":"0","unconfirmedTxApperances":0,"txApperances":2,"transactions":["7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75"]}`,
`{"page":1,"totalPages":1,"itemsOnPage":1000,"addrStr":"mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw","balance":"0","totalReceived":"12345.67890123","totalSent":"12345.67890123","unconfirmedBalance":"0","unconfirmedTxApperances":0,"txApperances":2,"transactions":["7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75"]}`,
},
},
{
name: "apiAddressUtxo",
r: newGetRequest(ts.URL + "/api/utxo/mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL"),
name: "apiAddress v2",
r: newGetRequest(ts.URL + "/api/v2/address/mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"),
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`{"page":1,"totalPages":1,"itemsOnPage":1000,"addrStr":"mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw","balance":"0","totalReceived":"1234567890123","totalSent":"1234567890123","unconfirmedBalance":"0","unconfirmedTxApperances":0,"txApperances":2,"txids":["7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75"]}`,
},
},
{
name: "apiAddressUtxo v1",
r: newGetRequest(ts.URL + "/api/v1/utxo/mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL"),
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`[{"txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","vout":1,"amount":"9172.83951061","satoshis":917283951061,"height":225494,"confirmations":1}]`,
},
},
{
name: "apiAddressUtxo v2",
r: newGetRequest(ts.URL + "/api/v2/utxo/mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL"),
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`[{"txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","vout":1,"value":"917283951061","height":225494,"confirmations":1}]`,
},
},
{
name: "apiSendTx",
r: newGetRequest(ts.URL + "/api/sendtx/1234567890"),
@ -475,7 +512,7 @@ func httpTests(t *testing.T, ts *httptest.Server) {
b := string(bb)
for _, c := range tt.body {
if !strings.Contains(b, c) {
t.Errorf("Page body does not contain %v, body %v", c, b)
t.Errorf("got %v, want to contain %v", b, c)
break
}
}
@ -483,7 +520,7 @@ func httpTests(t *testing.T, ts *httptest.Server) {
}
}
func socketioTests(t *testing.T, ts *httptest.Server) {
func socketioTests_BitcoinType(t *testing.T, ts *httptest.Server) {
type socketioReq struct {
Method string `json:"method"`
Params []interface{} `json:"params"`
@ -578,7 +615,7 @@ func socketioTests(t *testing.T, ts *httptest.Server) {
t.Errorf("Socketio error %v", err)
}
if resp != tt.want {
t.Errorf("Socketio resp %v, want %v", resp, tt.want)
t.Errorf("got %v, want %v", resp, tt.want)
}
})
}
@ -592,6 +629,6 @@ func Test_PublicServer_BitcoinType(t *testing.T) {
ts := httptest.NewServer(s.https.Handler)
defer ts.Close()
httpTests(t, ts)
socketioTests(t, ts)
httpTests_BitcoinType(t, ts)
socketioTests_BitcoinType(t, ts)
}

View File

@ -32,7 +32,7 @@
<tr>
<th>Contract</th>
<th>Tokens</th>
<th style="width: 15%;">No. Txs</th>
<th style="width: 15%;">Transfers</th>
</tr>
{{- range $et := $addr.Erc20Tokens -}}
<tr>