diff --git a/api/types.go b/api/types.go index 35c368a2..ee3c39f2 100644 --- a/api/types.go +++ b/api/types.go @@ -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"` diff --git a/api/typesv1.go b/api/typesv1.go new file mode 100644 index 00000000..54b9b363 --- /dev/null +++ b/api/typesv1.go @@ -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, + } +} diff --git a/api/worker.go b/api/worker.go index f453593b..94aa79a9 100644 --- a/api/worker.go +++ b/api/worker.go @@ -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, diff --git a/bchain/baseparser.go b/bchain/baseparser.go index 9af9dfc6..57f4617d 100644 --- a/bchain/baseparser.go +++ b/bchain/baseparser.go @@ -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 diff --git a/bchain/types.go b/bchain/types.go index cf565234..d14f9e0e 100644 --- a/bchain/types.go +++ b/bchain/types.go @@ -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 diff --git a/server/public.go b/server/public.go index da7efa65..8134cdd7 100644 --- a/server/public.go +++ b/server/public.go @@ -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 { diff --git a/server/public_test.go b/server/public_test.go index 5e782227..08595ec7 100644 --- a/server/public_test.go +++ b/server/public_test.go @@ -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) } diff --git a/static/templates/address.html b/static/templates/address.html index f9c34b4d..a9966dd0 100644 --- a/static/templates/address.html +++ b/static/templates/address.html @@ -32,7 +32,7 @@