diff --git a/server/public.go b/server/public.go index 7759164e..7cef62e4 100644 --- a/server/public.go +++ b/server/public.go @@ -565,70 +565,48 @@ func (s *PublicServer) explorerSpendingTx(w http.ResponseWriter, r *http.Request return errorTpl, nil, err } -func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { - var address *api.Address - var filter string - var fn = api.AddressFilterVoutOff - var err error - s.metrics.ExplorerViews.With(common.Labels{"action": "address"}).Inc() - if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { - page, ec := strconv.Atoi(r.URL.Query().Get("page")) - if ec != nil { - page = 0 - } - filter = r.URL.Query().Get("filter") - if len(filter) > 0 { - if filter == "inputs" { - fn = api.AddressFilterVoutInputs - } else if filter == "outputs" { - fn = api.AddressFilterVoutOutputs - } else { - fn, ec = strconv.Atoi(filter) - if ec != nil || fn < 0 { - filter = "" - fn = api.AddressFilterVoutOff - } - } - } - address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsOnPage, api.AccountDetailsTxHistoryLight, &api.AddressFilter{Vout: fn}) - if err != nil { - return errorTpl, nil, err - } - } - data := s.newTemplateData() - data.AddrStr = address.AddrStr - data.Address = address - data.Page = address.Page - data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(address.Page, address.TotalPages) - if filter != "" { - data.PageParams = template.URL("&filter=" + filter) - data.Address.Filter = filter - } - return addressTpl, data, nil -} - -func (s *PublicServer) getXpubAddress(r *http.Request, xpub string, pageSize int, option api.AccountDetails) (*api.Address, api.TokensToReturn, error) { - var fn = api.AddressFilterVoutOff +func (s *PublicServer) getAddressQueryParams(r *http.Request, accountDetails api.AccountDetails, maxPageSize int) (int, int, api.AccountDetails, *api.AddressFilter, string, int) { + var voutFilter = api.AddressFilterVoutOff page, ec := strconv.Atoi(r.URL.Query().Get("page")) if ec != nil { page = 0 } - filter := r.URL.Query().Get("filter") - if len(filter) > 0 { - if filter == "inputs" { - fn = api.AddressFilterVoutInputs - } else if filter == "outputs" { - fn = api.AddressFilterVoutOutputs + pageSize, ec := strconv.Atoi(r.URL.Query().Get("pageSize")) + if ec != nil || pageSize > maxPageSize { + pageSize = maxPageSize + } + from, ec := strconv.Atoi(r.URL.Query().Get("from")) + if ec != nil { + from = 0 + } + to, ec := strconv.Atoi(r.URL.Query().Get("to")) + if ec != nil { + to = 0 + } + filterParam := r.URL.Query().Get("filter") + if len(filterParam) > 0 { + if filterParam == "inputs" { + voutFilter = api.AddressFilterVoutInputs + } else if filterParam == "outputs" { + voutFilter = api.AddressFilterVoutOutputs } else { - fn, ec = strconv.Atoi(filter) - if ec != nil || fn < 0 { - fn = api.AddressFilterVoutOff + voutFilter, ec = strconv.Atoi(filterParam) + if ec != nil || voutFilter < 0 { + voutFilter = api.AddressFilterVoutOff } } } - gap, ec := strconv.Atoi(r.URL.Query().Get("gap")) - if ec != nil { - gap = 0 + switch r.URL.Query().Get("details") { + case "basic": + accountDetails = api.AccountDetailsBasic + case "tokens": + accountDetails = api.AccountDetailsTokens + case "tokenBalances": + accountDetails = api.AccountDetailsTokenBalances + case "txids": + accountDetails = api.AccountDetailsTxidHistory + case "txs": + accountDetails = api.AccountDetailsTxHistory } tokensToReturn := api.TokensToReturnNonzeroBalance switch r.URL.Query().Get("tokens") { @@ -639,32 +617,72 @@ func (s *PublicServer) getXpubAddress(r *http.Request, xpub string, pageSize int case "nonzero": tokensToReturn = api.TokensToReturnNonzeroBalance } - a, err := s.api.GetXpubAddress(xpub, page, pageSize, option, &api.AddressFilter{Vout: fn, TokensToReturn: tokensToReturn}, gap) - return a, tokensToReturn, err + gap, ec := strconv.Atoi(r.URL.Query().Get("gap")) + if ec != nil { + gap = 0 + } + return page, pageSize, accountDetails, &api.AddressFilter{ + Vout: voutFilter, + TokensToReturn: tokensToReturn, + FromHeight: uint32(from), + ToHeight: uint32(to), + }, filterParam, gap } -func (s *PublicServer) explorerXpub(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { - var address *api.Address - var tokensToReturn api.TokensToReturn - var err error - s.metrics.ExplorerViews.With(common.Labels{"action": "xpub"}).Inc() - if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { - address, tokensToReturn, err = s.getXpubAddress(r, r.URL.Path[i+1:], txsOnPage, api.AccountDetailsTxHistoryLight) - if err != nil { - return errorTpl, nil, err - } +func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { + var addressParam string + i := strings.LastIndexByte(r.URL.Path, '/') + if i > 0 { + addressParam = r.URL.Path[i+1:] + } + if len(addressParam) == 0 { + return errorTpl, nil, api.NewAPIError("Missing address", true) + } + s.metrics.ExplorerViews.With(common.Labels{"action": "address"}).Inc() + page, _, _, filter, filterParam, _ := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage) + // do not allow details to be changed by query params + address, err := s.api.GetAddress(addressParam, page, txsOnPage, api.AccountDetailsTxHistoryLight, filter) + if err != nil { + return errorTpl, nil, err } data := s.newTemplateData() data.AddrStr = address.AddrStr data.Address = address data.Page = address.Page data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(address.Page, address.TotalPages) - filter := r.URL.Query().Get("filter") - if filter != "" { - data.PageParams = template.URL("&filter=" + filter) - data.Address.Filter = filter + if filterParam != "" { + data.PageParams = template.URL("&filter=" + filterParam) + data.Address.Filter = filterParam } - data.NonZeroBalanceTokens = tokensToReturn == api.TokensToReturnNonzeroBalance + return addressTpl, data, nil +} + +func (s *PublicServer) explorerXpub(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { + var xpub string + i := strings.LastIndexByte(r.URL.Path, '/') + if i > 0 { + xpub = r.URL.Path[i+1:] + } + if len(xpub) == 0 { + return errorTpl, nil, api.NewAPIError("Missing xpub", true) + } + s.metrics.ExplorerViews.With(common.Labels{"action": "xpub"}).Inc() + page, _, _, filter, filterParam, gap := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage) + // do not allow txsOnPage and details to be changed by query params + address, err := s.api.GetXpubAddress(xpub, page, txsOnPage, api.AccountDetailsTxHistoryLight, filter, gap) + if err != nil { + return errorTpl, nil, err + } + data := s.newTemplateData() + data.AddrStr = address.AddrStr + data.Address = address + data.Page = address.Page + data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(address.Page, address.TotalPages) + if filterParam != "" { + data.PageParams = template.URL("&filter=" + filterParam) + data.Address.Filter = filterParam + } + data.NonZeroBalanceTokens = filter.TokensToReturn == api.TokensToReturnNonzeroBalance return xpubTpl, data, nil } @@ -865,64 +883,84 @@ func (s *PublicServer) apiBlockIndex(r *http.Request, apiVersion int) (interface } func (s *PublicServer) apiTx(r *http.Request, apiVersion int) (interface{}, error) { + var txid string + i := strings.LastIndexByte(r.URL.Path, '/') + if i > 0 { + txid = r.URL.Path[i+1:] + } + if len(txid) == 0 { + return nil, api.NewAPIError("Missing txid", true) + } var tx *api.Tx var err error s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx"}).Inc() - if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { - txid := r.URL.Path[i+1:] - spendingTxs := false - p := r.URL.Query().Get("spending") - if len(p) > 0 { - spendingTxs, err = strconv.ParseBool(p) - if err != nil { - return nil, api.NewAPIError("Parameter 'spending' cannot be converted to boolean", true) - } - } - tx, err = s.api.GetTransaction(txid, spendingTxs, false) - if err == nil && apiVersion == apiV1 { - return s.api.TxToV1(tx), nil + spendingTxs := false + p := r.URL.Query().Get("spending") + if len(p) > 0 { + spendingTxs, err = strconv.ParseBool(p) + if err != nil { + return nil, api.NewAPIError("Parameter 'spending' cannot be converted to boolean", true) } } + 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, apiVersion int) (interface{}, error) { + var txid string + i := strings.LastIndexByte(r.URL.Path, '/') + if i > 0 { + txid = r.URL.Path[i+1:] + } + if len(txid) == 0 { + return nil, api.NewAPIError("Missing txid", true) + } var tx json.RawMessage var err error s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx-specific"}).Inc() - if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { - txid := r.URL.Path[i+1:] - tx, err = s.chain.GetTransactionSpecific(&bchain.Tx{Txid: txid}) - } + tx, err = s.chain.GetTransactionSpecific(&bchain.Tx{Txid: txid}) return tx, err } func (s *PublicServer) apiAddress(r *http.Request, apiVersion int) (interface{}, error) { + var addressParam string + i := strings.LastIndexByte(r.URL.Path, '/') + if i > 0 { + addressParam = r.URL.Path[i+1:] + } + if len(addressParam) == 0 { + return nil, api.NewAPIError("Missing address", true) + } var address *api.Address var err error s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc() - if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { - page, ec := strconv.Atoi(r.URL.Query().Get("page")) - if ec != nil { - page = 0 - } - address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsInAPI, api.AccountDetailsTxidHistory, &api.AddressFilter{Vout: api.AddressFilterVoutOff}) - if err == nil && apiVersion == apiV1 { - return s.api.AddressToV1(address), nil - } + page, pageSize, details, filter, _, _ := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI) + address, err = s.api.GetAddress(addressParam, page, pageSize, details, filter) + if err == nil && apiVersion == apiV1 { + return s.api.AddressToV1(address), nil } return address, err } func (s *PublicServer) apiXpub(r *http.Request, apiVersion int) (interface{}, error) { + var xpub string + i := strings.LastIndexByte(r.URL.Path, '/') + if i > 0 { + xpub = r.URL.Path[i+1:] + } + if len(xpub) == 0 { + return nil, api.NewAPIError("Missing xpub", true) + } var address *api.Address var err error s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub"}).Inc() - if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { - address, _, err = s.getXpubAddress(r, r.URL.Path[i+1:], txsInAPI, api.AccountDetailsTxidHistory) - if err == nil && apiVersion == apiV1 { - return s.api.AddressToV1(address), nil - } + page, pageSize, details, filter, _, gap := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI) + address, err = s.api.GetXpubAddress(xpub, page, pageSize, details, filter, gap) + if err == nil && apiVersion == apiV1 { + return s.api.AddressToV1(address), nil } return address, err } diff --git a/server/public_test.go b/server/public_test.go index 2ed5a9ab..7e112421 100644 --- a/server/public_test.go +++ b/server/public_test.go @@ -461,7 +461,34 @@ func httpTests_BitcoinType(t *testing.T, ts *httptest.Server) { }, }, { - name: "apiXpub v2 tokens=nonzero", + name: "apiAddress v2 details=basic", + r: newGetRequest(ts.URL + "/api/v2/address/mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw?details=basic"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"address":"mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw","balance":"0","totalReceived":"1234567890123","totalSent":"1234567890123","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2}`, + }, + }, + { + name: "apiAddress v2 details=txs", + r: newGetRequest(ts.URL + "/api/v2/address/mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw?details=txs"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw","balance":"0","totalReceived":"1234567890123","totalSent":"1234567890123","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"transactions":[{"txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","vin":[{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","n":0,"addresses":["mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"],"value":"1234567890123"},{"txid":"00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840","vout":1,"n":1,"addresses":["mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz"],"value":"12345"}],"vout":[{"value":"317283951061","n":0,"spent":true,"hex":"76a914ccaaaf374e1b06cb83118453d102587b4273d09588ac","addresses":["mzB8cYrfRwFRFAGTDzV8LkUQy5BQicxGhX"]},{"value":"917283951061","n":1,"hex":"76a9148d802c045445df49613f6a70ddd2e48526f3701f88ac","addresses":["mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL"]}],"blockhash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockheight":225494,"confirmations":1,"blocktime":22549400000,"value":"1234567902122","valueIn":"1234567902468","fees":"346"},{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vin":[],"vout":[{"value":"1234567890123","n":0,"spent":true,"hex":"76a914a08eae93007f22668ab5e4a9c83c8cd1c325e3e088ac","addresses":["mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"]},{"value":"1","n":1,"spent":true,"hex":"a91452724c5178682f70e0ba31c6ec0633755a3b41d987","addresses":["2MzmAKayJmja784jyHvRUW1bXPget1csRRG"]},{"value":"9876","n":2,"spent":true,"hex":"a914e921fc4912a315078f370d959f2c4f7b6d2a683c87","addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"]}],"blockhash":"0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997","blockheight":225493,"confirmations":2,"blocktime":22549300001,"value":"1234567900000","valueIn":"0","fees":"0"}]}`, + }, + }, + { + name: "apiAddress v2 missing address", + r: newGetRequest(ts.URL + "/api/v2/address/"), + status: http.StatusBadRequest, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"error":"Missing address"}`, + }, + }, + { + name: "apiXpub v2 default", r: newGetRequest(ts.URL + "/api/v2/xpub/" + dbtestdata.Xpub), status: http.StatusOK, contentType: "application/json; charset=utf-8", @@ -487,6 +514,51 @@ func httpTests_BitcoinType(t *testing.T, ts *httptest.Server) { `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"txids":["3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75"],"totalTokens":2,"tokens":[{"type":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8,"balance":"0","totalReceived":"1","totalSent":"1"},{"type":"XPUBAddress","name":"2MsYfbi6ZdVXLDNrYAQ11ja9Sd3otMk4Pmj","path":"m/49'/1'/33'/0/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuAZNAjLSo6RLFad2fvHSfgqBD7BoEVy4T","path":"m/49'/1'/33'/0/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NEqKzw3BosGnBE9by5uaDy5QgwjHac4Zbg","path":"m/49'/1'/33'/0/3","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mw7vJNC8zUK6VNN4CEjtoTYmuNPLewxZzV","path":"m/49'/1'/33'/0/4","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N1kvo97NFASPXiwephZUxE9PRXunjTxEc4","path":"m/49'/1'/33'/0/5","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuWrWMzoBt8VDFNvPmpJf42M1GTUs85fPx","path":"m/49'/1'/33'/0/6","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuVZ2Ca6Da9zmYynt49Rx7uikAgubGcymF","path":"m/49'/1'/33'/0/7","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzRGWDUmrPP9HwYu4B43QGCTLwoop5cExa","path":"m/49'/1'/33'/0/8","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5C9EEWJzyBXhpyPHqa3UNed73Amsi5b3L","path":"m/49'/1'/33'/0/9","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzNawz2zjwq1L85GDE3YydEJGJYfXxaWkk","path":"m/49'/1'/33'/0/10","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N7NdeuAMgL57WE7QCeV2gTWi2Um8iAu5dA","path":"m/49'/1'/33'/0/11","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8JQEP6DSHEZHNsSDPA1gHMUq9YFndhkfV","path":"m/49'/1'/33'/0/12","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mvbn3YXqKZVpQKugaoQrfjSYPvz76RwZkC","path":"m/49'/1'/33'/0/13","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8MRNxCfwUY9TSW27X9ooGYtqgrGCfLRHx","path":"m/49'/1'/33'/0/14","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N6HvwrHC113KYZAmCtJ9XJNWgaTcnFunCM","path":"m/49'/1'/33'/0/15","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NEo3oNyHUoi7rmRWee7wki37jxPWsWCopJ","path":"m/49'/1'/33'/0/16","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mzm5KY8qdFbDHsQfy4akXbFvbR3FAwDuVo","path":"m/49'/1'/33'/0/17","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NGMwftmQCogp6XZNGvgiybz3WZysvsJzqC","path":"m/49'/1'/33'/0/18","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N3fJrrefndYjLGycvFFfYgevpZtcRKCkRD","path":"m/49'/1'/33'/0/19","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N1T7TnHBwfdpBoyw53EGUL7vuJmb2mU6jF","path":"m/49'/1'/33'/0/20","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzSBtRWHbBjeUcu3H5VRDqkvz5sfmDxJKo","path":"m/49'/1'/33'/1/0","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MtShtAJYb1afWduUTwF1SixJjan7urZKke","path":"m/49'/1'/33'/1/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N3cP668SeqyBEr9gnB4yQEmU3VyxeRYith","path":"m/49'/1'/33'/1/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"},{"type":"XPUBAddress","name":"2NEzatauNhf9kPTwwj6ZfYKjUdy52j4hVUL","path":"m/49'/1'/33'/1/4","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4RjsDp4LBpkNqyF91aNjgpF9CwDwBkJZq","path":"m/49'/1'/33'/1/5","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8XygTmQc4NoBBPEy3yybnfCYhsxFtzPDY","path":"m/49'/1'/33'/1/6","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5BjBomZvb48sccK2vwLMiQ5ETKp1fdPVn","path":"m/49'/1'/33'/1/7","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MybMwbZRPCGU3SMWPwQCpDkbcQFw5Hbwen","path":"m/49'/1'/33'/1/8","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N7HexL4dyAQc7Th4iqcCW4hZuyiZsLWf74","path":"m/49'/1'/33'/1/9","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NF6X5FDGWrQj4nQrfP6hA77zB5WAc1DGup","path":"m/49'/1'/33'/1/10","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4ZRPdvc7BVioBTohy4F6QtxreqcjNj26b","path":"m/49'/1'/33'/1/11","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mtfho1rLmevh4qTnkYWxZEFCWteDMtTcUF","path":"m/49'/1'/33'/1/12","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NFUCphKYvmMcNZRZrF261mRX6iADVB9Qms","path":"m/49'/1'/33'/1/13","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5kBNMB8qgxE4Y4f8J19fScsE49J4aNvoJ","path":"m/49'/1'/33'/1/14","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NANWCaefhCKdXMcW8NbZnnrFRDvhJN2wPy","path":"m/49'/1'/33'/1/15","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NFHw7Yo2Bz8D2wGAYHW9qidbZFLpfJ72qB","path":"m/49'/1'/33'/1/16","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NBDSsBgy5PpFniLCb1eAFHcSxgxwPSDsZa","path":"m/49'/1'/33'/1/17","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NDWCSQHogc7sCuc2WoYt9PX2i2i6a5k6dX","path":"m/49'/1'/33'/1/18","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8vNyDP7iSDjm3BKpXrbDjAxyphqfvnJz8","path":"m/49'/1'/33'/1/19","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4tFKLurSbMusAyq1tv4tzymVjveAFV1Vb","path":"m/49'/1'/33'/1/20","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NBx5WwjAr2cH6Yqrp3Vsf957HtRKwDUVdX","path":"m/49'/1'/33'/1/21","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NBu1seHTaFhQxbcW5L5BkZzqFLGmZqpxsa","path":"m/49'/1'/33'/1/22","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NCDLoea22jGsXuarfT1n2QyCUh6RFhAPnT","path":"m/49'/1'/33'/1/23","transfers":0,"decimals":8}]}`, }, }, + { + name: "apiXpub v2 details=basic", + r: newGetRequest(ts.URL + "/api/v2/xpub/" + dbtestdata.Xpub + "?details=basic"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":3,"totalTokens":2}`, + }, + }, + { + name: "apiXpub v2 details=tokens?tokens=used", + r: newGetRequest(ts.URL + "/api/v2/xpub/" + dbtestdata.Xpub + "?details=tokens&tokens=used"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":3,"totalTokens":2,"tokens":[{"type":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8},{"type":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8}]}`, + }, + }, + { + name: "apiXpub v2 details=tokenBalances", + r: newGetRequest(ts.URL + "/api/v2/xpub/" + dbtestdata.Xpub + "?details=tokenBalances"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":3,"totalTokens":2,"tokens":[{"type":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"}]}`, + }, + }, + { + name: "apiXpub v2 details=txs&tokens=derived&gap=5&from=225494&to=225494&pageSize=3", + r: newGetRequest(ts.URL + "/api/v2/xpub/" + dbtestdata.Xpub + "?details=txs&tokens=derived&gap=5&from=225494&to=225494&pageSize=3"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"page":1,"totalPages":1,"itemsOnPage":3,"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"transactions":[{"txid":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","vin":[{"txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","n":0,"addresses":["mzB8cYrfRwFRFAGTDzV8LkUQy5BQicxGhX"],"value":"317283951061"},{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":1,"n":1,"addresses":["2MzmAKayJmja784jyHvRUW1bXPget1csRRG"],"value":"1"}],"vout":[{"value":"118641975500","n":0,"hex":"a91495e9fbe306449c991d314afe3c3567d5bf78efd287","addresses":["2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu"]},{"value":"198641975500","n":1,"hex":"76a9143f8ba3fda3ba7b69f5818086e12223c6dd25e3c888ac","addresses":["mmJx9Y8ayz9h14yd9fgCW1bUKoEpkBAquP"]}],"blockhash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockheight":225494,"confirmations":1,"blocktime":22549400001,"value":"317283951000","valueIn":"317283951062","fees":"62"}],"totalTokens":2,"tokens":[{"type":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8,"balance":"0","totalReceived":"1","totalSent":"1"},{"type":"XPUBAddress","name":"2MsYfbi6ZdVXLDNrYAQ11ja9Sd3otMk4Pmj","path":"m/49'/1'/33'/0/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuAZNAjLSo6RLFad2fvHSfgqBD7BoEVy4T","path":"m/49'/1'/33'/0/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NEqKzw3BosGnBE9by5uaDy5QgwjHac4Zbg","path":"m/49'/1'/33'/0/3","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mw7vJNC8zUK6VNN4CEjtoTYmuNPLewxZzV","path":"m/49'/1'/33'/0/4","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N1kvo97NFASPXiwephZUxE9PRXunjTxEc4","path":"m/49'/1'/33'/0/5","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzSBtRWHbBjeUcu3H5VRDqkvz5sfmDxJKo","path":"m/49'/1'/33'/1/0","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MtShtAJYb1afWduUTwF1SixJjan7urZKke","path":"m/49'/1'/33'/1/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N3cP668SeqyBEr9gnB4yQEmU3VyxeRYith","path":"m/49'/1'/33'/1/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"},{"type":"XPUBAddress","name":"2NEzatauNhf9kPTwwj6ZfYKjUdy52j4hVUL","path":"m/49'/1'/33'/1/4","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4RjsDp4LBpkNqyF91aNjgpF9CwDwBkJZq","path":"m/49'/1'/33'/1/5","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8XygTmQc4NoBBPEy3yybnfCYhsxFtzPDY","path":"m/49'/1'/33'/1/6","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5BjBomZvb48sccK2vwLMiQ5ETKp1fdPVn","path":"m/49'/1'/33'/1/7","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MybMwbZRPCGU3SMWPwQCpDkbcQFw5Hbwen","path":"m/49'/1'/33'/1/8","transfers":0,"decimals":8}]}`, + }, + }, + { + name: "apiXpub v2 missing xpub", + r: newGetRequest(ts.URL + "/api/v2/xpub/"), + status: http.StatusBadRequest, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"error":"Missing xpub"}`, + }, + }, { name: "apiUtxo v1", r: newGetRequest(ts.URL + "/api/v1/utxo/mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL"),